泛型#

泛型(Generics) 讓函數和類別能夠在保有型別安全的前提下,操作不同的資料型別,避免重複撰寫僅資料型別不同的程式碼。


1. 為什麼需要泛型#

假設我們需要一個「容器」類別,如果不使用泛型,就必須為每種型別各寫一個:

1
2
class IntBox(val value: Int)
class StringBox(val value: String)

使用泛型,只需一個類別就能處理所有型別:

1
class Box<T>(val value: T)

2. 泛型類別#

宣告#

使用角括號 <T> 宣告型別參數,T 只是慣例命名,可以使用任意名稱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Box<T>(val value: T) {
    fun getValue(): T = value
}

fun main() {
    val intBox = Box(42)
    val strBox = Box("Hello")

    println(intBox.getValue()) // 輸出:42
    println(strBox.getValue()) // 輸出:Hello
}

多個型別參數#

1
2
3
4
5
6
7
8
class Pair<A, B>(val first: A, val second: B) {
    override fun toString() = "($first, $second)"
}

fun main() {
    val pair = Pair("Alice", 30)
    println(pair) // 輸出:(Alice, 30)
}

3. 泛型函數#

函數同樣可以宣告型別參數。

1
2
3
4
5
6
7
8
9
fun <T> printItem(item: T) {
    println("內容:$item,型別:${item!!::class.simpleName}")
}

fun main() {
    printItem(42)        // 輸出:內容:42,型別:Int
    printItem("Kotlin")  // 輸出:內容:Kotlin,型別:String
    printItem(3.14)      // 輸出:內容:3.14,型別:Double
}

泛型函數的實際應用:交換兩個元素#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun <T> swap(list: MutableList<T>, i: Int, j: Int) {
    val temp = list[i]
    list[i] = list[j]
    list[j] = temp
}

fun main() {
    val nums = mutableListOf(1, 2, 3, 4, 5)
    swap(nums, 0, 4)
    println(nums) // 輸出:[5, 2, 3, 4, 1]
}

4. 型別上界(Upper Bound)#

問題:沒有限制時能做什麼?#

沒有上界的泛型,編譯器只知道 T 是「某個型別」,無法呼叫任何特定方法。以下程式碼會編譯失敗:

1
2
3
fun <T> max(a: T, b: T): T {
    return if (a > b) a else b  // 錯誤:T 不一定支援 > 運算子
}

解法:用 : 指定上界#

<T : 上界型別> 中指定上界,代表 T 必須是該型別或其子類別,編譯器就能確保 T 擁有該型別的所有方法。

1
2
3
4
5
6
7
8
9
fun <T : Comparable<T>> max(a: T, b: T): T {
    return if (a > b) a else b  // 合法:Comparable<T> 提供了 > 運算子
}

fun main() {
    println(max(3, 7))              // 輸出:7
    println(max("apple", "banana")) // 輸出:banana
    println(max(3.14, 2.71))        // 輸出:3.14
}

Comparable<T> 是 Kotlin / Java 標準函式庫的介面,IntStringDouble 等型別都有實作它,所以這三種呼叫都合法。


上界阻擋不相容的型別#

假設自訂一個沒有實作 Comparable 的類別,傳入 max() 時編譯器會直接報錯:

1
2
3
4
5
6
class Point(val x: Int, val y: Int) // 未實作 Comparable

fun main() {
    // 錯誤:Point 不符合 Comparable<Point> 上界
    // max(Point(1, 2), Point(3, 4))
}

這正是上界的用意:在編譯期就攔截不合理的型別,而非等到執行時才崩潰


上界實際應用:限制為數字型別#

1
2
3
4
5
6
7
8
9
fun <T : Number> sum(list: List<T>): Double {
    return list.sumOf { it.toDouble() }
}

fun main() {
    println(sum(listOf(1, 2, 3)))           // 輸出:6.0
    println(sum(listOf(1.5, 2.5, 3.0)))     // 輸出:7.0
    // sum(listOf("a", "b"))  // 錯誤:String 不是 Number
}

預設上界#

沒有指定上界時,預設上界為 Any?(可為 null 的任意型別),代表幾乎沒有限制。


多個上界:where 子句#

當需要同時滿足多個條件時,用 where 子句列出,角括號只能寫一個上界。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Printable {
    fun printInfo()
}

// T 必須同時是 Comparable<T> 且實作 Printable
fun <T> printMax(a: T, b: T) where T : Comparable<T>, T : Printable {
    val bigger = if (a > b) a else b
    bigger.printInfo()
}

data class Score(val value: Int) : Comparable<Score>, Printable {
    override fun compareTo(other: Score) = value.compareTo(other.value)
    override fun printInfo() = println("分數:$value")
}

fun main() {
    printMax(Score(80), Score(95)) // 輸出:分數:95
}

5. 變異(Variance)#

變異描述了泛型型別與其型別參數之間的繼承關係。


協變(Covariant):out#

使用 out 修飾符表示型別參數只會被 輸出(回傳),不會被輸入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Producer<out T>(private val value: T) {
    fun produce(): T = value
}

fun main() {
    val intProducer: Producer<Int> = Producer(42)
    val anyProducer: Producer<Any> = intProducer // 合法,因為 out

    println(anyProducer.produce()) // 輸出:42
}

Kotlin 標準函式庫中的 List<out E> 就是協變的,所以 List<Int> 可以賦值給 List<Any>


逆變(Contravariant):in#

使用 in 修飾符表示型別參數只會被 輸入(接收),不會被輸出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Consumer<in T> {
    fun consume(value: T) {
        println("消費:$value")
    }
}

fun main() {
    val anyConsumer: Consumer<Any> = Consumer()
    val intConsumer: Consumer<Int> = anyConsumer // 合法,因為 in

    intConsumer.consume(42) // 輸出:消費:42
}

6. 星號投影(Star Projection)#

問題:List<Any> 並不萬用#

直覺上會想用 List<Any> 接受任何 List,但這樣行不通:

1
2
3
4
5
6
7
8
fun printList(list: List<Any>) {
    for (item in list) println(item)
}

fun main() {
    val ints: List<Int> = listOf(1, 2, 3)
    printList(ints) // 錯誤:List<Int> 無法傳給 List<Any>
}

因為 List<Int>List<Any> 在 Kotlin 中是 不同型別,即使 IntAny 的子類別。


解法:List<*> 星號投影#

* 代表「某個確定但未知的型別」,等同於 List<out Any?>。它告訴編譯器:「這個 List 裡裝著某種型別的元素,我不確定是什麼,但我只會讀,不會寫。」

1
2
3
4
5
6
7
8
9
fun printList(list: List<*>) {
    for (item in list) println(item)
}

fun main() {
    printList(listOf(1, 2, 3))        // 輸出:1 2 3
    printList(listOf("a", "b", "c"))  // 輸出:a b c
    printList(listOf(true, 3.14, "x")) // 混合型別也可以
}

星號投影只能讀,不能寫#

這是最重要的限制。因為編譯器不知道 * 實際是什麼型別,所以禁止任何寫入操作,防止型別不安全:

1
2
3
4
5
6
7
8
fun main() {
    val list: MutableList<*> = mutableListOf(1, 2, 3)

    println(list[0]) // 合法:讀取,得到 Any?

    list.add(4)      // 錯誤:不能寫入,編譯器不知道 * 是什麼型別
    list[0] = 99     // 錯誤:同上
}

星號投影 vs. Any vs. 泛型 T 比較#

寫法意義可讀可寫
List<Any>明確裝 Any 的 List
List<*>裝某個未知型別的 List✅(得到 Any?
List<T>裝已知型別 T 的 List

實際用途:只關心容器本身,不關心元素型別#

當函數只需要知道集合的 長度、是否為空、列印內容,而完全不需要操作元素型別時,* 是最適合的選擇。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun describeList(list: List<*>) {
    println("元素數量:${list.size}")
    println("是否為空:${list.isEmpty()}")
    println("第一個元素:${list.firstOrNull()}")
}

fun main() {
    describeList(listOf(10, 20, 30))
    // 輸出:
    // 元素數量:3
    // 是否為空:false
    // 第一個元素:10

    describeList(emptyList<String>())
    // 輸出:
    // 元素數量:0
    // 是否為空:true
    // 第一個元素:null
}

7. 實際應用:泛型 Repository#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Repository<T> {
    private val items = mutableListOf<T>()

    fun add(item: T) {
        items.add(item)
    }

    fun getAll(): List<T> = items.toList()

    fun findFirst(predicate: (T) -> Boolean): T? {
        return items.firstOrNull(predicate)
    }
}

data class User(val id: Int, val name: String)

fun main() {
    val repo = Repository<User>()
    repo.add(User(1, "Alice"))
    repo.add(User(2, "Bob"))
    repo.add(User(3, "Carol"))

    val found = repo.findFirst { it.id == 2 }
    println(found) // 輸出:User(id=2, name=Bob)

    println(repo.getAll())
    // 輸出:[User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Carol)]
}

Reference#

https://kotlinlang.org/docs/generics.html