你有沒有遇到過這樣的問題:
“為什么子類方法可以返回
Cat
,而父類只寫了返回Animal
?”
“為什么參數反而能從CatFood
變成更寬泛的Food
?”
這些看似“違反直覺”的設計,其實背后有一個優雅的編程概念:協變與逆變。
別被名字嚇到!今天我們不用術語堆砌,而是用一個“動物收容所”的故事,把這兩個概念講得清清楚楚,并明確說明它們在不同 PHP 版本中的支持情況。
🏡 故事開始:開一家動物收容所
假設你開了一個“動物收容所”,專門幫助流浪貓狗找主人。
你定義了一個基本的規則:
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)——返回值可以“更具體”
你設計了一個接口:
interface AnimalShelter {public function adopt(string $name): Animal;
}
意思是:任何收容所,都能領養一只“動物” 。
但具體實現時:
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)——參數可以“更寬泛”
接下來,你給動物加個“吃飯”功能。
class Food {}
class AnimalFood extends Food {}abstract class Animal {public function eat(AnimalFood $food) {echo $this->name . " 吃 " . get_class($food);}
}
所有動物都吃“動物糧”(AnimalFood
)。
但狗比較不挑食,它說:“我連普通食物都能吃!”
于是你重寫狗的方法:
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 的屬性是“死板”的:
class Parent {public Animal $pet;
}class Child extends Parent {public Dog $pet; // ? 不行!類型不能變
}
因為屬性既要“讀”又要“寫”:
- “讀”希望返回更具體的類型(協變)
- “寫”希望接受更寬泛的類型(逆變)
兩者沖突,所以只能“不變”。
但從 PHP 8.4 開始,我們可以定義“只讀”或“只寫”屬性!
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 完整支持)