從繼承關係聊聊 Kotlin 的 In & Out

Batu Tasvaluan
Dcard Tech Blog
Published in
16 min readNov 20, 2023

前言

相信在用 Kotlin 使用 Generic Type 的時候,一定會碰到需要用到 <in T>or <out T> 的狀況,但往往不是很清楚為什麼要這樣用,這樣用到底有什麼幫助,到底對 Generic Type 造成什麼影響…,觀察網路上的資料,大多是從 in & out 的功用來討論,而今天想要嘗試從 繼承關係 的角度,切入這個議題

有人看得懂 GPT 在說什麼嗎?

Before Everything start

直接先破題結論

你想要擴展類別的繼承關係,就用 out 吧

先用 out 就對了

協變與逆變 / Covariance and Contravariance

不免俗,還是要談一下這個專有名詞。根據 wiki,Variance 是用來描述複雜類別(Complex Types) 之間的繼承關係 (Subtyping),而 Covariance & Contravariance 則其中的兩種

val ArrayList<Animal> = ArrayList<Alien> 

上述的語法是否合法,取決於程式語言是否支援 Covariance & Contravariance

在這裡,我想要擅自擴大一下 variance 的定義:若是不僅限於 Conplex Type ,我們可以說:

Kotlin / Java / OOP 本身就是支援 協變 (covariance )

註:這個說法不是嚴謹的定義,只是想幫助我們思考繼承關係對指派(Assignment)的影響

以下面的範例 Code 為例,因為 Kotlin 的繼承關係,子類實體可以指派給父類別,這樣的行為,就是協變 covariance。反之就是逆變 Contravariance 。

var animal: Animal? = null

animal = Bug() // 沒問題

/*****************************/

fun getAnimal(): Animal {
return Bug() // 沒問題
}

/*****************************/

val bug: Bug = Animal() // 如果這個指派合法,表示這個語言支援逆變

回來討論 Complex Type 的狀況。當我們要使用到泛型(Generic Type)的時候,這項特性就會消失。你不能將實體指派給其他類別。因此,可以這樣理解:他們之間沒有繼承關係。

val animals: ArrayList<Animal> = ArrayList<Bug>() // ERROR! Type mismatch.
val bugs: ArrayList<Bug> = ArrayList<Bug>() // OK

BUT,我們工程師內心還是期盼著,當我們用 Genetic Type 的時候,Complext Type 還是能維持類似的繼承關係。或者是說:我想要上面那行 Code 不要報錯!這個時候,就要:

就用 out 吧

用個數學式表達,加強印象

  • 假如 A 是一個繼承自 P 的 class,那我們可以這樣表達: A ≤ P
  • 假如我們想表示一個使用 Generic Type 的 Complex Type ,例如 List<String>,我們可以這樣表達:f(x)
  • 那協變(covariance)就是: f(A) ≤ f(P) (維持繼承關係)
  • 而逆變(Contravariance)就是:f(A) ≥ f(P) (逆轉繼承關係)

所以最上面破題的結論,我們可以這樣說

想要 f(A) ≤ f(P) ,就用 out

為什麼是 Out !

在開始解釋之前,先記著口訣

out → 把東西丟出去 → 只能做 getter
in → 把東西吸進來 → 只能做 setter

這裡會用三個 Class 來舉例:Corgi, Dog, and Animal,其繼承關係如下:

// Corgi ≤ Dog ≤ Animal

open class Animal()
open class Dog() : Animal()
class Corgi() : Dog()

若我們用一個 Dog 實體,指派給這三個類別,其狀況如下,只有指派給 Corgi 時會出事

val animal: Animal = Dog()  // covariance
val dog : Dog = Dog()
val corgi : Corgi = Dog() // Error!

但若是使用了 Genetic Type ,就只能指派給完全一樣的類別

val animals: ArrayList<Animal> = ArrayList<Dog>()  // Error!
val dogs : ArrayList<Dog> = ArrayList<Dog>()
val corgis : ArrayList<Corgi> = ArrayList<Dog>() // Error!

但是,因為 val animal: Animal = dog() 是成立的,所以我們也會希望 val ancenstors: ArrayList<Animal> = ArrayList<Dog>() 能成立,也能夠成立,也就是 維持繼承關係

val animals: ArrayList<Animal> = ArrayList<Dog>()  // 想要這個也合法
val dogs : ArrayList<Dog> = ArrayList<Dog>()
val Corgis : ArrayList<Corgi> = ArrayList<Dog>() // Error!

為了達到這個目的,Kotlin 提供 Generic Type 支援 Covariance 的方法,也就是 <out T>。但為了達到此目的,Kotlin 會限制你:『此時該類別只能支援 getter』

Generic Type with Covariance

目標:維持繼承關係

假設這裡有一個支援 Covariance 的類別 DogHandler,此時 Kotlin 會限制 DogHandler 只能實作 getter

interface Handler<out T> {
fun getInstance(): T?
}

class DogHandler : Handler<Dog> {
private var dog: Dog? = null

override fun getInstance(): Dog? {
return dog
}
}

仿造上述的格式,若指派給三種 Complex Type 類別,其結果如下

val animalHandler: Handler<Animal> = DogHandler() // OK!
val dogHandler : Handler<Dog> = DogHandler()
val corgiHandler : Handler<Corgi> = DogHandler() // Error!

animalHandler.getInstance() // 沒有問題

如果 Setter 還在會發生什麼事

val animalHandler: Handler<Animal> = DogHandler()

// scenario 1
val dog = Dog()
animalHandler.setInstance(dog) // 可以

// scenario 2
val alien = Alien()
animalHandler.setInstance(alien) // 靠杯了
  • Scenario 1:想要把 Dog 塞到 Handler<Animal> 的 reference 裡面。Dog 也算是 Animal,類別沒問題。實際上的 instance 也是 Handler<Dog> 型態,的確能放進去。沒問題!
  • Scenario 2:想要把 Alien 塞到 Handler<Animal> 的 reference 裡面。Alien 也算是 Animal,類別沒問題,但是要把 Alien 塞進 Handler<Dog> 型態的 instance !? 感覺不太妙…
  • 在 Kotlin 的繼承系統下,若是要讓支援 Covariance 的 Complex Type 也開放使用 setter,就有可能會發生 type mismatch error

那 Getter 不會有問題嗎

  • 這裡的 animalHandler,指向的其實是一個 Handler<Dog> 的 instance
  • 在呼叫 animalHandler.getInstance() 的時候,其實是呼叫了 Handler<Dog>.getInstance(),所以其實是拿到一個 Dog instance
  • 但是,Kotlin 是支援 covariance 的,拿到的 instance 既是 Dog ,也同時是 Animal,所以沒有問題
  • 換句話說,animalHandler 其實也不知道實際拿到的 instance 是誰,但可以確定是得到一個 Animal 的 instance

為了要讓 Generic Type 支援 Covariance,就得限制不能使用 setter,不然會完蛋

Generic Type with Contravariance

目標:反轉逆轉關係

假設現在讓 DogHandler 支援 Contravariance,此時 Kotlin 會現在 DogHandler 只能支援 setter

interface Handler<in T>{
fun setInstance(instance: T)
}


class DogHandler: Handler<Dog> {
private var dog: Dog? = null

override fun setInstance(instance: Dog) {
this.dog = instance
}

}

若指派給三種 Complex Type 類別,其結果如下

val animalHandler: Handler<Animal> = DogHandler() // Error!
val dogHandler : Handler<Dog> = DogHandler()
val corgiHandler : Handler<Corgi> = DogHandler() // OK!

val corgi = Corgi()
corgiHandler.setInstance(corgi) // 沒有問題

如果 Getter 還在會發生什麼事

val corgiHandler : Handler<Corgi> = DogHandler()
val corgi = corgiHandler.getInstance()
  • corgiHandler 指向的是 Handler<Dog>
  • corgiHandler 呼叫 getInstance() 實際上是呼叫到 Handler<Dog>.getInstance(),所以你只會拿到 Dog 物件
  • 但依據 covariance (繼承關係) ,Corgi ≤ Dog,這個指派是不合法的

那 Setter 不會有問題嗎

  • 這裡的 corgiHandler,指向的是一個 Handler<Dog> 的 instance
  • 在呼叫 setInstance() 的時候,其實是呼叫了 Handler<Dog>.setInstance(),所以現在的操作其實是把 corgi instance 傳進 Dog type 的 function
  • Kotlin 是支援 covariance 的 (Corgi 是繼承自 Dog),要傳進去的 instance 既是 Corgi,也是 Dog,所以沒有問題
  • 換句話說,corgiHandler 其實也不知道 instance 實作為何,但可以確定是,只要傳進 Corgi type 一定沒問題

為了要讓 Generic Type 支援 Contravariance,就得限制不能使用 getter,不然會完蛋

結論

  • 以往我們比較像是遇到 type mismatch error,就嘗試看看要用 out or in。去查 out & in 的定義,卻只是知道這樣類別會變成生產者或消費者,但也不知道這跟我們原本的問題有什麼關係
  • out or in 是為了擴展其繼承關係所必須要採取策略,而這個策略能幫我們解決的問題是『拓展繼承關係』的需求
  • 目標是 若 A ≤ P,則 f(A) ≤ f(P) → 維持繼承關係 → Covariance → 需要限制 setter 的使用 → 要用 out → 變成了生產者
  • 目標是 若 A ≤ P,則 f(A) ≥ f(P) → 反轉繼承關係 → Contravariance → 需要限制 getter 的使用 → 要用 in → 變成了消費者
  • 通常來說,我們 87% 遇到的狀況,是套用 Generic Type 之後,依舊維持繼承關係,所以就是一開始破題的那句話:

你想要擴展類別的繼承關係,就用 out 吧

等等,那 in 勒

我們已經知道了什麼時候要用 out ,那什麼時候要用 in 呢?什麼時候我們會需要反轉繼承關係呢?恩…這個問題我也不清楚 :P,目前我自己還沒有遇到情境是要用 in 來實作,頂多是一樣繼承的 class 原本就要求,於是就順著使用 in,目前猜想可能是系統架構更龐大的時候會有這種反種繼承關係的需求。如果有人有好的情境,也歡迎跟我們分享 :D

我自己觀察目前使用 in 的情境,稍微可以理解成:借用 in 的效果,讓物件強制只能實作 setter,不能用 getter。例如以下的 Code:

interface DataFetcher<T> {
override fun loadData(
priority: Priority,
callback: DataFetcher.DataCallback<in T>
)
}

/*****************/

class AvatarFetcher() : DataFetcher<BitmapDrawable> {

override fun loadData(
priority: Priority,
callback: DataFetcher.DataCallback<in BitmapDrawable>
) {
try {
val bitmapDrawable = BitmapDrawable()
callback.onDataReady(bitmapDrawable)
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}

}

但如果不用 in ,會有問題嗎…好像也不會…恩… 等我參透了我再寫下一個文章來分享好了

等等…事情好像不總是這麼美好…

  • 若是依據這樣的標準,使用了 out / in ,並限制了 getter / setter,結果還是發生 type mismatch 的問題,那就表示你的使用情境太複雜,generic type variance 已經無能為力
真的嗎?
  • 或是,用 @UnsafeVariance。基本上就是叫 Compiler 不要管那麼多。事實上很多存在的類別也都有使用,以 Kotlin 的 List Interface 為例,他是支援 Covariance,但因為 contains function,不得不傳 instance 進來,於是就使用了@UnsafeVariance,如此以來就可以 compile 了,同時也就不保證 type safe 了

關於 in / out 的作用對象

如果你剛開始接觸 Variance ,可能很容易因為上述的解說,有了錯誤的印象:用了 in / out 之後,List 就可以 add 子類 或是 父類 ,但其實不是。List(或是其他使用 Generic Type 的 Complex Type),在呼叫 add(或其他 setter) 的時候,只能塞該類別及其子類。就跟沒有使用 in / out 的時候一樣

class Handler<T> { }     // 沒有使用 Variance 的情境

val dogHandler: Handler<Dog> = Handler<Dog>()
dogHandler.add(Animal()) // Type mismatch!
dogHandler.add(Dog()) // OK
dogHandler.add(Corgi()) // OK

/******************************************/

class Handler<in T> { } // 使用 Contravariance

val dogHandler: Handler<Dog> = Handler<Dog>()
dogHandler.add(Animal()) // Type mismatch!
dogHandler.add(Dog()) // OK
dogHandler.add(Corgi()) // OK

他實際影響的是 List<T1> 與 List<T2> 這兩個 Complex Type 的繼承關係,也就是這兩個類別的指派(assignment) 是否合法

class Handler<T> { }

val dogHandler: Handler<Dog> = Handler<Animal>() // Type mismatch!
val dogHandler: Handler<Dog> = Handler<Corgi>() // Type mismatch!

/******************************************/

class Handler<in T> { }

val dogHandler: Handler<Dog> = Handler<Animal>() // 變合法了

/******************************************/

class Handler<out T> { }

val dogHandler: Handler<Dog> = Handler<Corgi>() // 變合法了

最後,來思考一下吧

稍微反芻一下,離開前,來想想以下的問題吧:

A) Kotlin 的 List Interface,為什麼會用 out 呢?

B) 那為什麼 MutableList 就不宣告 out 呢?

是因為希望他們成為生產者嗎?還是希望能延展 Generic Type 的繼承關係?

希望這篇文章能對你有幫助喔~

Reference

--

--