## Ковариантность и контравариантность

В PHP 7.2.0 путём снятия ограничений типа для параметров в дочернем методе добавили частичную контравариантность. Начиная с PHP 7.4.0 добавили полную поддержку ковариантности и контравариантности.

Ковариантность разрешает дочернему методу возвращать более конкретный тип, чем тип значения возврата родительского метода. Контравариантность разрешает определить тип параметра в дочернем методе менее конкретным, чем в родительском.

Объявление типа считают более конкретным в следующем случае:

- Удалили тип из объединения типов
- Добавили тип к пересечению типов
- Тип класса изменили на тип дочернего класса
- Тип iterable изменили на тип array или Traversable

Класс типа считают менее конкретным, если верно обратное.

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

Создадим простой абстрактный родительский класс Animal, чтобы проиллюстрировать работу ковариантности. Класс 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 . " лает";
        }
    }

    class Cat extends Animal
    {
        public function speak()
        {
            echo $this->name . " мяукает";
        }
    }

    ?>

Обратите внимание, пример не содержит методов, которые возвращают значения. Добавим ряд фабрик, которые возвращают новый объект с типом класса Animal, Cat или Dog.

    <?php

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

    class CatShelter implements AnimalShelter
    {
        public function adopt(string $name): Cat // Возвращаем тип Cat вместо типа Animal
        {
            return new Cat($name);
        }
    }

    class DogShelter implements AnimalShelter
    {
        public function adopt(string $name): Dog // Возвращаем тип Dog вместо типа Animal
        {
            return new Dog($name);
        }
    }

    $kitty = (new CatShelter())->adopt("Рыжик");
    $kitty->speak();
    echo "\n";

    $doggy = (new DogShelter())->adopt("Бобик");
    $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 . " ест " . get_class($food);
        }
    }

    ?>

Переопределим метод eat в классе Dog так, чтобы метод принимал любой объект с типом Food, чтобы увидеть суть контравариантности. Класс Cat оставим без изменений.

    <?php

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

    ?>

Следующий пример покажет поведение контравариантности.

    <?php

    $kitty = (new CatShelter())->adopt("Рыжик");
    $catFood = new AnimalFood();
    $kitty->eat($catFood);
    echo "\n";

    $doggy = (new DogShelter())->adopt("Бобик");
    $banana = new Food();
    $doggy->eat($banana);

    ?>

Результат выполнения приведённого примера:

    Рыжик ест AnimalFood
    Бобик ест Food

Но что произойдёт, если кошка $kitty попробует методом eat() съесть банан $banana?

    $kitty->eat($banana);

Результат выполнения приведённого примера:

    Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given

### Вариативность свойств

По умолчанию свойства не удовлетворяют ни правилам ковариантности, ни правилам контравариантности, следовательно, свойства — инвариантны. Поэтому тип свойства вообще нельзя изменить в дочернем классе. Причина этого состоит в том, что операции «чтения» должны быть ковариантными, а операции «записи» — контравариантными. Единственный доступный для свойства способ удовлетворить обоим требованиям — оставаться инвариантным.

Начиная с PHP 8.4.0, в котором добавили абстрактные свойства в интерфейсе или абстрактном классе и виртуальные свойства, разрешается объявить свойство, доступное только для операций чтения или записи. Итогом нововведений стало то, что абстрактным свойствам или виртуальным свойствам, для которых требуется только операция "get", доступна ковариантность. Аналогично, абстрактному свойству или виртуальному свойству, для которого требуется только операция "set", доступна контравариантность.

Однако как только для свойства объявили как операцию get, так и операцию set, свойство теряет ковариантность или контравариантность для расширения, и снова становится инвариантным.

Пример #1 Пример вариативности типа свойства

    <?php

    class Animal {}
    class Dog extends Animal {}
    class Poodle extends Dog {}

    interface PetOwner
    {
        // Требуется только операция get, поэтому свойству доступна ковариантность
        public Animal $pet {
            get;
        }
    }

    class DogOwner implements PetOwner
    {
        // Свойству возможно указать более ограниченный тип, поскольку со стороны операции get
        // по-прежнему возвращается Animal. Однако, поскольку это внутреннее свойство текущего класса,
        // потомкам класса больше нельзя изменять тип свойства
        public Dog $pet;
    }

    class PoodleOwner extends DogOwner
    {
        // Изменение свойства НЕДОПУСТИМО, поскольку для свойства DogOwner::$pet
        // определили поведение операций get и set, и дочерние классы обязаны соблюдать
        // контракт родительского класса при переопределении свойства
        public Poodle $pet;
    }

    ?>

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open