Skip to content

Instantly share code, notes, and snippets.

@rayshih
Last active July 7, 2024 09:01
Show Gist options
  • Select an option

  • Save rayshih/7b7bb9dd28fec505cc7bb64fa2bd3a3f to your computer and use it in GitHub Desktop.

Select an option

Save rayshih/7b7bb9dd28fec505cc7bb64fa2bd3a3f to your computer and use it in GitHub Desktop.

你可能其實不懂繼承 - 什麼是 Covariance 跟 Contravariance

在近代高等程式語言快(ㄏㄨˋ)速(ㄒ一ㄤ)演(ㄔㄠ)化(ㄒㄧˊ)的狀況下,很多 strong type 的程式語言都開始有了 generic 的設計, 舉例來說:

  • Mobile: Swift, Kotlinn
  • Frontend: Flowtype, TypeScript
  • 後端語言:Python (pyre), Scala, Java (沒錯!現在連 Java 都有)

Generic type 雖然看起來是比較新的東西,但如果要做到嚴格的 strong type 基本上是少不了的。 例如 Array/List 如果沒有 generic 的話,只能有兩種做法:

  1. 把所有東西都轉成 Object or Any type 才能塞進 List,然後在從 List 取出物件的時候再做 down casting。
  2. 針對需要的 object type 個別實作相對應的 List type,例如:Cat 就有 CatList。

第一個方法的問題是:你有可能 down cast 到錯的 type,譬如說你本來塞的是 Cat,但你拿出來後 cast 成 Dog。 這樣就會噴 Runtime Error。而第二種方法的確是 type safe ,但因為每個 type 都要重寫一堆邏輯,非常不方便。

現在我們知道 generic 的方便性及重要性了。接下來我們來進入本篇想討論的:Covariance 跟 Contravariance。

基本上, generic 的出現代表了 type system 增加了一個新的維度,碰上有 subtype 的 type system 會變得比平常複雜。 偏偏現代幾乎所有程式語言都有 inheritance,所以這變成了現在 programmer 早晚會碰到的問題。我聽到的 case 都幾乎是走 「碰到就想辦法繞過」這種解決方式。而個人在工作上也有碰到因為對 generic 一知半解而設計出來得詭異架構。要了解什麼是 covariance ,我們必須先了解 generic 碰上 subtype 會出現什麼樣的問題,其中最經典的:

List<Cat> 是不是 List<Animal> 的 subtype?

乍看之下答案是「當然是啊,CatAnimal,當然 List<Cat> 就是 List<Animal> 啊」,但實際上並沒有這麼簡單。 我們先假設 List<Cat>List<Animal> 這件事情成立好了,這就代表:

List<Cat> listOfCat = // 總之,某個方法生出來的,不是重點所以略過
List<Animal> listOfAnimal = listOfCat;

以上,根據假設沒問題,但接下來的 code 就不合理了:

listOfAnimal.add(new Dog());

光看這一行可能沒問題,但同時考慮前段 code 就能看出錯誤:listOfAnimal 實際上是 listOfCat,而其 type 是 List<Cat>。 也就是,你正在把 dog 塞進一個只能放 Cat 的 List,而如果其他地方還持有 listOfCat 的 reference 的話,就有可能拿到 dog, 而 dog 不是 cat,當然就是個 type error。

既然如此,那反過來說

List<Cat> 能不能是 List<Animal> 的 supertype?

答案是....看情況。

那麼什麼情況下不行呢?

如果你今天試著從 list 拿值出來就不行。

Cat cat = listOfCat.get(0) // 取得 Animal

因為 listOfCat 如果是 listOfAnimal 的 supertype 的話,代表 listOfCat 有可能實際上是 listOfAnimal, 也就是你取到的值有可能是 Animal 而不是 Cat,而 Animal 並不是 Cat 的 subtype,於是這邊會產生另外一個 type error。

所以以常識上的通用 List 來說的話,List<Cat> 既不是 List<Animal> 的 subtype 也不是 supertype。

那假設我們想做一個特殊的 List 使得 List<Cat>List<Animal> 的 subtype 呢?

Covariance

首先,在上述的例子中我們已經知道如果我們可以「塞」東西進去這個 List , 那就不可能做到「List<Cat>List<Animal> 的 subtype」。 所以我們要另外定一個 list 叫做 ConstList,意思是這個 list 是 constant,也就是我們不能改動裡面的值。 你可能會問:「既然這個 list 不能塞東西進去,那我要這個 list 幹麻?」,雖然不能塞東西進去, 但是你可以在產生這個 list 的時候就把值設定好,例如

ConstList<Cat> list = new ConstList<Cat>(cat1, cat2, cat3);

那麼,我們要怎麼告訴 typechecker 說「假設 A 是 B 的 subtype,那麼 ConstList<A> 就是 ConstList<B> 的 subtype」呢?

以下舉幾個程式語言當例子:

FlowType

(其實我本來想要用 TypeScript 說明的,結果一查資料才發現 TypeScript 在這塊實作其實是有問題的 issue 1394

class ConstList<+T> { ... }

Kotlin

class ConstList<out T> { ... }

Java

Java 無法在宣告 class 時宣告是否為 covariant,只能在宣告變數時使用。

ConstList<? extends Animal> list = new ConstList<Cat>()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment