协变与逆变

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

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

在以下情况下,类型声明被认为更具体:

  • 联合类型 中删除类型
  • 类类型修改为子类类型
  • float 修改为 int
  • iterable 修改为 array 或者 Traversable
如果情况相反,则类型类被认为是模糊的。

协变

创建一个名为 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