协变和逆变

来源 https://www.php.net/manual/zh/language.oop5.variance.php

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

:::danger 重要

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

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

联合类型 中删除类型 在 交集类型 中添加类型 类类型(class type)修改为子类类型

iterable 修改为 array 或者 Traversable 如果情况相反,则类型类被认为是模糊的。

协变

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

1<?php
2
3abstract class Animal
4{
5    protected string $name;
6
7    public function __construct(string $name)
8    {
9        $this->name = $name;
10    }
11
12    abstract public function speak();
13}
14
15class Dog extends Animal
16{
17    public function speak()
18    {
19        echo $this->name . " barks";
20    }
21}
22
23class Cat extends Animal 
24{
25    public function speak()
26    {
27        echo $this->name . " meows";
28    }
29}

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

1<?php
2
3interface AnimalShelter
4{
5    public function adopt(string $name): Animal;
6}
7
8class CatShelter implements AnimalShelter
9{
10    public function adopt(string $name): Cat // 返回类的类型不仅限于 Animal,还可以是 Cat 类型
11    {
12        return new Cat($name);
13    }
14}
15
16class DogShelter implements AnimalShelter
17{
18    public function adopt(string $name): Dog // 返回类的类型不仅限于 Animal,还可以是 Dog 类型
19    {
20        return new Dog($name);
21    }
22}
23
24
25
26$kitty = (new CatShelter)->adopt("Ricky");
27$kitty->speak();
28echo "\n";
29
30$doggy = (new DogShelter)->adopt("Mavrick");
31$doggy->speak();

以上示例会输出:

1Ricky meows
2Mavrick barks

逆变

继续上一个例子,除了 Animal、 Cat、Dog,我们还添加了 Food、AnimalFood 类, 同时为抽象类 Animal 添加了一个 eat(AnimalFood $food) 方法。

1<?php
2
3class Food {}
4
5class AnimalFood extends Food {}
6
7abstract class Animal
8{
9    protected string $name;
10
11    public function __construct(string $name)
12    {
13        $this->name = $name;
14    }
15
16    public function eat(AnimalFood $food)
17    {
18        echo $this->name . " eats " . get_class($food);
19    }
20}
21?>

为了演示什么是逆变,Dog 类重写(overridden)了 eat 方法, 允许传入任意 Food 类型的对象。 而 Cat 类保持不变。

1<?php
2
3class Dog extends Animal
4{
5    public function eat(Food $food) {
6        echo $this->name . " eats " . get_class($food);
7    }
8}
9?>

下面的例子展示了逆变。

1<?php
2
3$kitty = (new CatShelter)->adopt("Ricky");
4$catFood = new AnimalFood();
5$kitty->eat($catFood);
6echo "\n";
7
8$doggy = (new DogShelter)->adopt("Mavrick");
9$banana = new Food();
10$doggy->eat($banana);
11以上示例会输出:
12
13Ricky eats AnimalFood
14Mavrick eats Food
15$kitty 若尝试 eat() $banana 会发生什么呢?
16
17$kitty->eat($banana);
18?>

以上示例会输出:

1Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given

属性差异

默认情况下,属性既不是协变也不是逆变,因此是不变的。也就是说,它们的类型在子类中可能根本不会改变。原因是“get”操作必须是协变的,而“set”操作必须是逆变的。属性满足这两个要求的唯一方法是不变的。

自 PHP 8.4.0 起,随着抽象属性(在接口或抽象类上)和虚拟属性的增加,可以声明仅具有 get 或 set 操作的属性。因此,仅需“get”操作的抽象属性或虚拟属性可能是协变的。同样,仅需“set”操作的抽象属性或虚拟属性可能是逆变的。

然而,一旦属性同时具有 get 和 set 操作,就不再是协变或逆变的了,无法进一步扩展。也就是说,现在是不变的。

示例 #1 属性类型差异

1<?php
2class Animal {}
3class Dog extends Animal {}
4class Poodle extends Dog {}
5
6interface PetOwner
7{
8    // 只需要 get 操作,因此这可能是协变的。
9    public Animal $pet { get; }
10}
11
12class DogOwner implements PetOwner
13{
14    // 这可能是一个更严格的类型,
15    // 因为“get”端仍返回 Animal。但是,作为原生属性,
16    // 此类的子类可能无法再更改类型。
17    public Dog $pet;
18}
19
20class PoodleOwner extends DogOwner
21{
22    // 这是不允许的,因为 DogOwner::$pet
23    // 已经定义并要求 get 和 set 操作。
24    public Poodle $pet;
25}
26?>
Copyright @ 2024 ~ 2025 czfadmin