协变与逆变

在 PHP 7.2.0 里,通过对子类方法里参数的类型放宽限制,实现对逆变的部分支持。 自 PHP 7.4.0 起开始支持完整的协变和逆变。

协变使子类比父类方法能返回更具体的类型; 逆变使子类比父类方法参数类型能接受更模糊的类型。

协变

创建一个名为 Animal 的简单的抽象父类,用于演示什么是协变。 两个子类:CatDog 扩展(extended)了 Animal

<?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";
    }
}

注意:在这个例子中,没有方法返回了值。 将通过添加个别工厂方法,创建并返回 AnimalCatDog 类型的新对象。

<?php

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

class 
CatShelter implements AnimalShelter
{
    public function 
adopt(string $name): Cat // 返回类的类型不仅限于 Animal,还可以是 Cat 类型
    
{
        return new 
Cat($name);
    }
}

class 
DogShelter implements AnimalShelter
{
    public function 
adopt(string $name): Dog // 返回类的类型不仅限于 Animal,还可以是 Dog 类型
    
{
        return new 
Dog($name);
    }
}

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

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

以上例程会输出:

Ricky meows
Mavrick barks

逆变

继续上一个例子,除了 AnimalCatDog,我们还添加了 FoodAnimalFood 类, 同时为抽象类 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 类重写(overridden)了 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);

以上例程会输出:

Ricky eats AnimalFood
Mavrick eats 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

User Contributed Notes

maxim dot kainov at gmail dot com 04-Apr-2021 05:57
This example will not work:

<?php

class CatFood extends AnimalFood { }

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

?>

The reason is:

<?php
   
class DogFood extends AnimalFood { }
  
    function
feedAnimal(Animal $animal, AnimalFood $food) {
       
$animal->eat($food);  
    }

   
$cat = new Cat();
   
$dogFood = new DogFood();  

   
feedAnimal($cat, $dogFood);   
?>

But you can do it with traits, like this:

<?php

trait AnimalTrait
{
    public function
eat(AnimalFood $food)
    {
        echo
$this->name . " ест " . get_class($food);
    }
}

class
Cat
{
    use
AnimalTrait;

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

?>
phpnet-at-kennel17-dotco-dotuk 20-Jan-2021 05:51
Following the examples above, you might assume the following would be possible.

<?php

class CatFood extends AnimalFood { ... }

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

?>

However, the Liskov Substitution Prinicpal, and therefore PHP, forbids this.  There's no way for cats to eat cat food, if animals are defined as eating animal food.

There are a large number of legitimate abstractions that are forbidden by PHP, due to this restriction.
Anonymous 12-Feb-2020 08:47
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>';
xedin dot unknown at gmail dot com 13-Jan-2020 04:20
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`.