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):inout

這是 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 解析、資料庫查詢等框架中非常實用。