在前幾章的程式碼裡,常常會出現 <T>
這樣的標記,這個 T
代表任一型別(Type),而這個型別是可以讓外部決定的。這種標記稱為泛型(Generic),對於 Kotlin 這種強型別語言來說,泛型讓程式可以彈性地接受各種型別,讓開發者在設計時有更高的彈性外,同時也讓編譯器能確保型別的正確性。Collection 類別之所以可以放入不同型別的元素,也是因為底層在實作時採用泛型。在這個章節裡,我們就透過 Trace Collection 的原始碼來深入了解泛型為何?
打開 IntelliJ IDEA,用 listOf()
宣告一個 List
後,追蹤一下原始碼,就會看到這段:
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
listOf()
接受多個相同型別的參數,將這些參數裝在 List
後回傳。從以上這段程式碼可以看到,當一個函式可以接受泛型參數時,會在型別的部份以 T
標示,並在 fun
關鍵字後面以 <T>
宣告。當程式實際運作,會將這些 T
改成實際的型別,以一個字串清單來說,在「概念上」程式就會變成這樣:
public fun <String> listOf(vararg elements: String): List<String>
泛型參數通常以大寫 T
(意指 Type)宣告,雖然可以用任意字母,不過因為支援泛型的語言都已約定俗成採用 T
,所以除非有需求,不然建議遵守這個慣例。而在設計 Collection 或 Interface 時,習慣使用 E
(意指 Element);而當 Collection 是 Map 時,則會使用 K
(意指 Key)及 V
(意指 Value);若函式回傳的是泛型型別的話,則會使用 R
(意指 Return)。
上面的原始碼示範的是在函式裡定義泛型函數,若是一個類型要支援泛型呢?像 List<String>
這樣的原始碼是怎麼寫出來的?追蹤一下 Collection 的原始碼可以看到這段 List Interface 的定義:
public interface List<out E> : Collection<E> {
// ...
}
要在一個介面或類別裡支援泛型的話,則是在名稱後面接 <T>
來宣告。若想要限制泛型是某一種型別或它的子型別,則可以用 <T: 類別名稱>
宣告。
class MyList<T> {
// ...
}
class MyPetCollection<T: Pet> {
// ...
}
若是需要支援多個泛型類別的話,可以在角括號裡以逗號分隔多個泛型來宣告。
class MyMap<K, V> {
// ...
}
in
與 out
來支援多型眼尖的你應該有發現,在 List 原始碼裡的 <out E>
泛型宣告裡多了一個 out
關鍵字,這是什麼呢?
Kotlin 天生支援多型,也就是說,若 Apple
繼承自 Fruit
,在參數傳遞的時候,若參數指定型別為 Fruit
,則傳入 Apple
也是可以的,因為 Apple
勢必會實作所有 Fruit
的 API。但這點在泛型裡是不成立的,換句話說,若參數為 MyList<Fruit>
則傳入 MyList<Apple>
是不行的。
聽起來不太合理?Kotlin 之所以會這樣設計,是因為在像以下這種情境下,有可能會破壞類型系統。
fun add(bowl: Bowl<Fruit>, fruit: Fruit) = bowl.add(fruit)
val bowl = Bowl<Apple>()
add(bowl, Pear()) // 實際上無法通過編譯
val apple = bowl.get() // 爆!
若你真的希望可以支援多型,則需要像 List 原始碼裡一樣,在泛型前面加一個 out
來宣告 covariant,如此子類型就可以取代父類型。若是情境反過來,需要用父類型來取代子類型,則要改用 in
來宣告 contravariant。
Collection 類別之所以可以支援多型,也是因為在泛型參數設計時就有加上 out
,所以以下程式碼才有辦法通過編譯器。
val lisfOfApple: List<Fruit> = listOf<Apple>()
在這章裡我們追了一下 List
的原始碼,了解到之所以 Collection 可以彈性地放入各種型別是因為有泛型的設計,而且還能讓編譯器保持對型別推斷和提示的能力。學會這招後,以後有機會設計函式庫的時候,別忘了善用泛型這個法寶喔!