iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Software Development

新手也能懂的 Kotlin Collection 賞玩門道系列 第 26

第二十六天:深入 Collection 核心 - 泛型

在前幾章的程式碼裡,常常會出現 <T> 這樣的標記,這個 T 代表任一型別(Type),而這個型別是可以讓外部決定的。這種標記稱為泛型(Generic),對於 Kotlin 這種強型別語言來說,泛型讓程式可以彈性地接受各種型別,讓開發者在設計時有更高的彈性外,同時也讓編譯器能確保型別的正確性。Collection 類別之所以可以放入不同型別的元素,也是因為底層在實作時採用泛型。在這個章節裡,我們就透過 Trace Collection 的原始碼來深入了解泛型為何?

初探 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> {
    // ...
}

inout 來支援多型

眼尖的你應該有發現,在 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 可以彈性地放入各種型別是因為有泛型的設計,而且還能讓編譯器保持對型別推斷和提示的能力。學會這招後,以後有機會設計函式庫的時候,別忘了善用泛型這個法寶喔!

參考資料


上一篇
第二十五天:深入 Collection 核心 - 效能評估
下一篇
第二十七天:深入 Collection 核心 - Lambda
系列文
新手也能懂的 Kotlin Collection 賞玩門道31

尚未有邦友留言

立即登入留言