Covariance and Contravariance in PHP

Ковариантност

Covariance allows a child’s method to return a more specific type than the return type of its parent’s method.

Whereas, contravariance allows a parameter type to be less specific in a child method, than that of its parent.

Ако си представим една йерархична структура от наследяващи се класове, covariance означава, че „децата“ връщат по-конкретен тип резултат.

По-просто, covariance „затяга конкретността“, contravariance – обратното.

Какво значи „more specific“ (по-голяма конкретност)?
1. когато не се използват т.н. union types (изброени с „|“ типове);
2. ако класът, който използваме за тип, има родител, не използваме родителя;
3. вместо float, използваме int (ако случаят го позволява разбира се), защото int е частен случай на float.

Както се вижда, това е основната логика зад понятието „ковариантност“.

Ако обърнем логиката обратно – към „разширяване“ или „намаляване на конкретността“ – това пък е „контравариантност“.

Ето много прост пример за ковариантност:

<?php

abstract class Animal
{
    public function __construct(protected string $name) { }
    abstract public function speak();
}

class Dog extends Animal
{
    public function speak(): void
{
        echo $this->name . " barks";
    }
}

class Cat extends Animal 
{
    public function speak(): void
{
        echo $this->name . " meows";
    }
}

// Lest have a simple factory, that produces cats or dogs...
function makeMeADog(): Dog
{
return new Dog('Bobi');
}

function makeMeACat(): Cat
{
return new Cat('Pisko');
}

Къде е ковариантността?
Можем да променим Animal да не е абстрактен, а нормален клас и да си „произвеждаме“ животни.
Но решаваме да сме по-конкретни, искаме не Животно, а конкретно Куче, Котка… и… това е.

Контравариантност

Пример за контравариантност би бил ако например в някой от оверрайдващите методи, зададем като тип на входен параметър – по-малко конкретен тип. Тоест, ако наследникът „разшири“ вместо да „затегне“ конкретността.

class Food { }

class AnimalFood extends Food { }

abstract class Animal
{
    public function __construct(protected string $name) { }

    public function eat(AnimalFood $food): void
{
        echo $this->name . " eats " . get_class($food);
    }
}

class Dog extends Animal
{
    public function eat(Food $food): void
 {
        echo $this->name . " eats " . get_class($food);
    }
}

Вижда се как в Dog, наследникът на Animal, в оверрайдващият метод eat() позволяваме входен параметър Food с по-малка конкретност.
Това ще да е някое всеядно куче 😉

Литература:

https://www.php.net/manual/en/language.oop5.variance.php