协变和逆变

在 PHP 7.2.0 中,通过移除子方法中参数的类型限制,引入了部分逆变。从 PHP 7.4.0 开始,添加了完全协变和逆变支持。

协变允许子方法返回比其父方法返回值类型更具体的类型。而逆变允许子方法的参数类型比其父方法的参数类型更不具体。

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

如果相反的情况成立,则类型类被认为更不具体。

协变

为了说明协变的工作原理,创建了一个简单的抽象父类 AnimalAnimal 将被子类 CatDog 扩展。

<?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 的类,并将函数 eat(AnimalFood $food) 添加到 Animal 抽象类中。

<?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 类中重写了 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
添加注释

用户贡献的注释 3 条注释

xedin dot unknown at gmail dot com
4 年前
我想解释为什么协变和逆变很重要,以及为什么它们分别适用于返回类型和参数类型,而不是相反。

协变可能最容易理解,并且与 Liskov 替换原则直接相关。使用上面的例子,假设我们收到一个 `AnimalShelter` 对象,然后我们想通过调用它的 `adopt()` 方法来使用它。我们知道它返回一个 `Animal` 对象,无论该对象到底是什么,即它是 `Cat` 还是 `Dog`,我们都可以以相同的方式对待它们。因此,专门化返回类型是可以的:我们至少知道任何可以返回的东西的通用接口,并且我们可以以相同的方式对待所有这些值。

逆变稍微复杂一些。它与提高方法灵活性的实用性密切相关。再次使用上面的例子,也许“基础”方法 `eat()` 接受一种特定类型的食物;但是,_特定_动物可能希望支持_更广泛_的食物类型。也许它,就像上面的例子一样,为原始方法添加了功能,允许它消耗_任何_种类的食物,而不仅仅是为动物准备的食物。`Animal` 中的“基础”方法已经实现了允许它消耗为动物专门设计的食物的功能。`Dog` 类中的覆盖方法可以检查参数是否为 `AnimalFood` 类型,并简单地调用 `parent::eat($food)`。如果参数_不是_专门类型的,它可以对该参数执行额外的甚至完全不同的处理 - 不会破坏原始签名,因为它_仍然_处理专门类型,但也处理更多。这就是为什么它也与 Liskov 替换密切相关:消费者仍然可以将专门的食物类型传递给 `Animal`,而不知道它到底是 `Cat` 还是 `Dog`。
Hayley Watson
1 年前
Liskov 替换原则如何应用于类类型的要点基本上是:“如果一个对象是某个东西的实例,那么它应该能够在允许使用某个东西实例的任何地方使用”。当你记住“某个东西”可能是该对象的父类时,协变和逆变规则就来自于这种预期。

对于文本中的 Cat/Animal 示例,Cats 是 Animals,因此 Cats 应该能够去任何 Animals 可以去的地方。变异规则将此形式化。

协变:子类可以覆盖父类中的方法,该方法具有更窄的返回类型。(返回类型可以在更具体的子类中更具体;它们“沿相同方向变化”,因此称为“协变”。)
如果一个对象有一个你期望产生 Animals 的方法,你应该能够用一个该方法只产生 Cats 的对象来替换它。你只会从中得到 Cats,但 Cats 是 Animals,这就是你期望从对象中得到的。

逆变:子类可以覆盖父类中的方法,该方法具有参数更宽的类型。(参数可以在更具体的子类中更不具体;它们“沿相反方向变化”,因此称为“逆变”。)
如果一个对象有一个你期望接受 Cats 的方法,你应该能够用一个接受任何类型 Animals 的对象来替换它。你只会给它 Cats,但 Cats 是 Animals,这就是对象期望从你那里得到的。

所以,如果你的代码正在使用某个类的对象,并且它给定一个子类的实例来使用,它不应该造成任何麻烦。
它可能接受任何类型的 Animal,而你只给它 Cats,或者它可能只返回 Cats,而你很乐意接受任何类型的 Animal,但 LSP 认为“那又怎样?Cats 是 Animals,所以你们都应该满意。”
匿名
4 年前
协变也适用于一般类型提示,注意还有接口

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