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

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

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

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

В противном случае класс типа считается менее конкретным.

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

Чтобы проиллюстрировать, как работает ковариантность, создадим простой абстрактный родительский класс 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 
To Top