Covariance and Contravariance

Оновлено: 11.05.2023

У PHP 7.2.0 було введено часткову контраваріантність шляхом зняття обмежень на типи параметрів у дочірньому методі. Починаючи з версії PHP 7.4.0, було додано повну підтримку коваріації та контраваріантності.

Коваріантність дозволяє дочірньому методу повертати більш конкретний тип, ніж тип повернення батьківського методу. Натомість, протиріччя дозволяє зробити тип параметра в дочірньому методі менш специфічним, ніж у батьківському методі.

У наступному випадку оголошення типу вважається більш специфічним:

Тип видаляється з типу об'єднання Тип додається до типу перетину Тип класу змінюється на тип дочірнього класу iterable змінено на array або Traversable

Щоб проілюструвати, як працює коваріація, створено простий абстрактний батьківський клас Animal. Тварина буде розширена дочірніми класами Cat та Dog.

<?php

abstract class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    abstract public function speak();
}

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

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

Зверніть увагу, що у цьому прикладі немає методів, які повертають значення. Буде додано декілька фабрик, які повертають новий об'єкт класу типу Animal, Cat або Dog.

<?php

interface AnimalShelter
{
    public function adopt(string $name): Animal;
}

class CatShelter implements AnimalShelter
{
    public function adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
    {
        return new Cat($name);
    }
}

class DogShelter implements AnimalShelter
{
    public function adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
    {
        return new Dog($name);
    }
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();

Продовжуючи попередній приклад з класами Animal, Cat і Dog, буде включено клас під назвою Food і AnimalFood, а до абстрактного класу Animal додано метод eat(AnimalFood $food).

<?php

class Food {}

class AnimalFood extends Food {}

abstract class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

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

Для того, щоб побачити поведінку протиріччя, у класі Dog перевизначено метод eat, щоб дозволити будь-який об'єкт типу Food. Клас Cat залишається без змін.

<?php

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

Наступний приклад покаже поведінку протиріччя.

<?php

$kitty = (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);

Але що станеться, якщо $kitty спробує з'їсти() $banana?

$kitty->eat($banana);