你有没有遇到过这样的问题:
"为什么子类方法可以返回
Cat
,而父类只写了返回Animal
?""为什么参数反而能从
CatFood
变成更宽泛的Food
?"
这些看似"违反直觉"的设计,其实背后有一个优雅的编程概念:协变与逆变。
别被名字吓到!今天我们不用术语堆砌,而是用一个"动物收容所"的故事,把这两个概念讲得清清楚楚,并明确说明它们在不同 PHP 版本中的支持情况。
🏡 故事开始:开一家动物收容所
假设你开了一个"动物收容所",专门帮助流浪猫狗找主人。
你定义了一个基本的规则:
scala
abstract class Animal {
protected string $name;
public function __construct(string $name) {
$this->name = $name;
}
abstract public function speak();
}
class Cat extends Animal {
public function speak() {
echo $this->name . " 喵喵叫";
}
}
class Dog extends Animal {
public function speak() {
echo $this->name . " 汪汪叫";
}
}
一切都很正常。现在,你想让收容所支持"领养"功能。
🌱 第一幕:协变(Covariance)------返回值可以"更具体"
你设计了一个接口:
php
interface AnimalShelter {
public function adopt(string $name): Animal;
}
意思是:任何收容所,都能领养一只"动物" 。
但具体实现时:
php
class CatShelter implements AnimalShelter {
public function adopt(string $name): Cat {
return new Cat($name);
}
}
class DogShelter implements AnimalShelter {
public function adopt(string $name): Dog {
return new Dog($name);
}
}
注意!父接口说"返回 Animal
",子类却返回了更具体的 Cat
或 Dog
。
❓这合法吗?
✅ 合法!这就是"协变" 。
✅ 协变的核心思想:
返回值可以变得更"具体" 。
就像你说:"我要领养一只动物。"
收容所说:"给你一只猫。"
👉 没问题!猫当然是动物。
🔍 技术上:
Cat
是Animal
的子类,所以更"窄"、更"具体",返回它是安全的。
这是 协变(Covariance) :
协 = 协同,方向一致 ------ 类型从"动物"变成"猫",越来越具体,方向一致。
注意 :完整的协变支持是从 PHP 7.4 开始的。
🍽️ 第二幕:逆变(Contravariance)------参数可以"更宽泛"
接下来,你给动物加个"吃饭"功能。
scala
class Food {}
class AnimalFood extends Food {}
abstract class Animal {
public function eat(AnimalFood $food) {
echo $this->name . " 吃 " . get_class($food);
}
}
所有动物都吃"动物粮"(AnimalFood
)。
但狗比较不挑食,它说:"我连普通食物都能吃!"
于是你重写狗的方法:
scala
class Dog extends Animal {
public function eat(Food $food) { // 参数变宽了!
echo $this->name . " 吃 " . get_class($food);
}
}
父类要求传 AnimalFood
,子类却接受更宽泛的 Food
!
❓这合法吗?
✅ 也合法!这就是"逆变" 。
✅ 逆变的核心思想:
参数可以变得更"宽泛" 。
就像你去吃饭,菜单写"本店只接受现金"。
但店长说:"其实刷卡、支付宝我们也收。"
👉 更包容了,没问题!
🔍 技术上:
Food
是AnimalFood
的父类,范围更广。狗能吃的东西更多,说明它更"包容",不会破坏原有规则。
这是 逆变(Contravariance) :
逆 = 相反 ------ 继承是"子类 → 父类",但参数类型却从"子类"变回"父类",方向相反。
注意 :部分逆变支持是从 PHP 7.2 开始的,但完整的逆变支持也是从 PHP 7.4 开始的。
🧩 第三幕:属性的"读写困境"
以前,PHP 的属性是"死板"的:
scala
class Parent {
public Animal $pet;
}
class Child extends Parent {
public Dog $pet; // ❌ 不行!类型不能变
}
因为属性既要"读"又要"写":
- "读"希望返回更具体的类型(协变)
- "写"希望接受更宽泛的类型(逆变)
两者冲突,所以只能"不变"。
但从 PHP 8.4 开始,我们可以定义"只读"或"只写"属性!
kotlin
interface PetOwner {
public Animal $pet { get; } // 只读
}
class DogOwner implements PetOwner {
public Dog $pet; // ✅ 可以!只读 → 协变成立
}
因为只允许"读",所以返回更具体的 Dog
是安全的。
✅ 只读 → 协变
✅ 只写 → 逆变
❌ 可读可写 → 不变
📝 总结:一张表看懂协变与逆变
场景 | 能不能变? | 如何变? | 生活例子 | 支持版本 |
---|---|---|---|---|
返回值 | ✅ 协变 | 越来越具体(Animal → Cat) | "动物" → "猫" | PHP 7.4+ |
参数 | ✅ 逆变 | 越来越宽泛(AnimalFood → Food) | "只能吃动物粮" → "啥都能吃" | PHP 7.4+ (部分支持从 PHP 7.2 开始) |
属性(只读) | ✅ 协变 | 可以更具体 | "宠物" → "狗" | PHP 8.4+ |
属性(可读可写) | ❌ 不变 | 类型不能变 | 既要读又要写,不能乱改 | - |
💡 为什么要有协变和逆变?
为了让代码更灵活 又安全。
- 协变让你能返回更具体的对象,便于后续调用具体方法。
- 逆变让你的子类更包容,适应更多输入。
- 它们共同保证:子类不会破坏父类的契约。
🎉 结语
协变与逆变,听起来高深,其实很简单:
- 协变:返回值 → 越来越"小"(具体)
- 逆变:参数 → 越来越"大"(宽泛)
记住这个口诀:
🔤 "出(返回)要具体,入(参数)要包容"
从 PHP 7.4 开始,这些特性让你的面向对象编程更加优雅、类型更安全。
现在,你已经不是"听不懂协变逆变"的人了,而是那个能讲清楚的人!👏
📌 适合读者 :PHP 初学者、中级开发者、想理解类型系统的你
📅 适用版本:PHP 7.4+(逆变从 7.2 开始部分支持,7.4 完整支持)