Kotlin 泛型 (Generics)
泛型 (Generics) 是強型別語言中非常強大的功能。它讓我們可以寫出「適用於多種型別」的通用程式碼,同時還能享有編譯器的型別檢查保護。
為什麼需要泛型?
假設我們要設計一個可以裝任何東西的盒子 Box。如果不使用泛型,我們只能用 Any:
class Box(var content: Any)
val box = Box("Hello")
val str = box.content as String // 必須強轉,如果不小心轉錯會 crash
使用泛型後,我們可以告訴編譯器「這個盒子只裝 String」:
class Box<T>(var content: T)
val box = Box<String>("Hello")
val str = box.content // 自動推斷為 String,不需要轉型,絕對安全
泛型類別 (Generic Classes)
定義泛型類別時,在類別名稱後加上 <T> (T 代表 Type,你也可以用其他名稱):
// 定義
class Container<T>(val items: List<T>) {
fun get(index: Int): T {
return items[index]
}
}
// 使用
val intContainer = Container<Int>(listOf(1, 2, 3))
val strContainer = Container(listOf("A", "B")) // 自動推斷 T 為 String
泛型函式 (Generic Functions)
泛型不一定要綁在類別上,也可以單獨定義在函式。
泛型參數 <T> 要放在 fun 關鍵字之後、函式名稱之前。
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
val list = singletonList(100) // T 為 Int
泛型限制 (Generic Constraints)
有時候我們不希望 T 是任意型別,而是希望它「至少是某種型別的子類別」。這時可以使用 上界 (Upper Bounds)。
單一限制 (:)
預設的上界是 Any?。如果我們限制 T 必須是數字:
// T 必須繼承自 Number (如 Int, Double, Float)
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
sum(1, 2) // OK
// sum("A", "B") // 編譯錯誤!String 不是 Number
多重限制 (where)
如果需要多個條件 (例如:必須繼承自 A 類別,且 實作 B 介面),可以使用 where 子句:
// T 必須是 CharSequence 且 可比較 (Comparable)
fun <T> sortString(items: List<T>)
where T : CharSequence,
T : Comparable<T> {
// ...
}
型別變異 (Variance):in 與 out
這是 Kotlin 泛型最抽象但也最重要的概念。
簡單來說,這解決了 List<String> 到底是不是 List<Any> 的子類別的問題。
預設情況下,泛型是 不變的 (Invariant)。
這意味著 Box<String> 不是 Box<Any> 的子類別。
out (Covariance / 協變)
如果你只有一個 唯讀 的容器,那麼 List<String> 視為 List<Any> 的子類別是很合理的(因為取出來的 String 也是 Any)。
在 Kotlin 中,使用 out 關鍵字來標記。這代表這個型別參數 只能被輸出 (Produced / Return Type),不能被輸入 (Consumed / Argument Type)。
// 只有 get() 回傳 T,沒有 set(item: T)
class Producer<out T>(val value: T) {
fun get(): T = value
}
val strProducer: Producer<String> = Producer("Hi")
val anyProducer: Producer<Any> = strProducer // 合法!因為 T 是 out
in (Contravariance / 逆變)
反過來,如果你有一個 唯寫 的容器,那麼 Comparable<Any> 視為 Comparable<String> 的子類別也是合理的(因為能比較 Any 的比較器,一定也能比較 String)。
使用 in 關鍵字標記。代表型別參數 只能被輸入 (Consumed / Argument Type),不能被輸出。
interface Consumer<in T> {
fun consume(item: T)
}
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
override fun consume(item: Any) = println(item)
}
val strConsumer: Consumer<String> = anyConsumer // 合法!
總結口訣
- PECS (Producer Extends, Consumer Super) 是 Java 的術語。
- Kotlin 簡化為:Consumer in, Producer out。
- 只讀取不寫入 -> 用
out(生產者) - 只寫入不讀取 -> 用
in(消費者)
- 只讀取不寫入 -> 用
星號投影 (Star Projection)
當你不知道 (或不關心) 具體的泛型型別是什麼,但仍想安全地使用它時,可以用 *。
這類似於 Java 的 ? (Raw Type 或 Wildcard)。
fun printList(list: List<*>) {
// 這裡視為 List<Any?>,雖然取出來的詳細型別不明,但至少是個物件
val item = list[0]
println(item)
}
- 對於
Foo<out T>,Foo<*>等同於Foo<out Any?>(可以讀出 Any?)。 - 對於
Foo<in T>,Foo<*>等同於Foo<in Nothing>(不能寫入任何東西)。
具現化型別 (Reified Type Parameters)
泛型在 JVM 上有一個限制:型別擦除 (Type Erasure)。
這意味著 List<String> 和 List<Int> 在執行時期其實都是 List,T 的資訊不見了。
所以你 不能 寫 if (obj is T)。
但在 Kotlin 中,如果你使用 inline function,搭配 reified 關鍵字,編譯器會把實際的型別「貼」進去,讓你可以在執行時期存取 T!
inline fun <reified T> isType(value: Any): Boolean {
// 這裡竟然可以使用 T 當作類別來檢查!
return value is T
}
fun main() {
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
}
這在 JSON 解析、資料庫查詢等框架中非常實用。