PHP 8.0.0 Beta 4 available for testing

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

В 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
add a note add a note

User Contributed Notes 2 notes

up
10
xedin dot unknown at gmail dot com
8 months ago
I would like to explain why covariance and contravariance are important, and why they apply to return types and parameter types respectively, and not the other way around.

Covariance is probably easiest to understand, and is directly related to the Liskov Substitution Principle. Using the above example, let's say that we receive an `AnimalShelter` object, and then we want to use it by invoking its `adopt()` method. We know that it returns an `Animal` object, and no matter what exactly that object is, i.e. whether it is a `Cat` or a `Dog`, we can treat them the same. Therefore, it is OK to specialize the return type: we know at least the common interface of any thing that can be returned, and we can treat all of those values in the same way.

Contravariance is slightly more complicated. It is related very much to the practicality of increasing the flexibility of a method. Using the above example again, perhaps the "base" method `eat()` accepts a specific type of food; however, a _particular_ animal may want to support a _wider range_ of food types. Maybe it, like in the above example, adds functionality to the original method that allows it to consume _any_ kind of food, not just that meant for animals. The "base" method in `Animal` already implements the functionality allowing it to consume food specialized for animals. The overriding method in the `Dog` class can check if the parameter is of type `AnimalFood`, and simply invoke `parent::eat($food)`. If the parameter is _not_ of the specialized type, it can perform additional or even completely different processing of that parameter - without breaking the original signature, because it _still_ handles the specialized type, but also more. That's why it is also related closely to the Liskov Substitution: consumers may still pass a specialized food type to the `Animal` without knowing exactly whether it is a `Cat` or `Dog`.
up
1
Anonymous
7 months ago
Covariance also works with general type-hinting, note also the interface:

interface xInterface
{
    public function y() : object;
}

abstract class x implements xInterface
{
    abstract public function y() : object;
}

class a extends x
{
    public function y() : \DateTime
    {
        return new \DateTime("now");
    }
}

$a = new a;
echo '<pre>';
var_dump($a->y());
echo '</pre>';
To Top