Kotlin 知識梳理(12) 泛型型別引數

澤毛發表於2017-12-21

一、本文概要

本文是對<<Kotlin in Action>>的學習筆記,如果需要執行相應的程式碼可以訪問線上環境 try.kotlinlang.org,這部分的思維導圖為:

Kotlin 知識梳理(12)   泛型型別引數

二、泛型型別引數

泛型允許你定義帶 型別形參 的型別,當這種型別的例項被建立出來的時候,型別形參被替換成為 型別實參 的具體型別。

Java不同,Kotlin始終要求型別實參要麼被顯示地說明,要麼能被編譯器推匯出來。例如,在Java中可以宣告List型別的變數,而不需要說明它可以包含哪些事物,而Kotlin從一開始就有泛型,所以它不支援原生態型別,型別實參必須定義

2.1 泛型函式和屬性

如果需要編寫一個使用列表的函式,希望它可以在任何列表上使用,需要編寫一個泛型函式,泛型函式有它自己的型別形參,這些 型別形參 在每次呼叫時都必須替換成具體的 型別實參

2.1.1 slice

例如集合當中slice函式的定義:

fun <T> List<T>.slice(indices : IntRange) : List<T>
複製程式碼

接收者和返回型別都用到了函式的型別形參T,它們的型別都是List<T>,當在一個具體的列表上呼叫這個函式時,可以顯示地指定型別實參,也可以讓編譯器自動推匯出型別:

fun main(args: Array<String>) {
    val letters = ('a'..'z').toList()
    //顯示地指定型別實參。
    println(letters.slice<Char>(0..2))
    //編譯器自動推匯出T的型別是Char。
    println(letters.slice(10..13))
}
複製程式碼

2.1.2 filter

下面再來看filter的例子

fun main(args: Array<String>) {
    val authors = listOf("first", "second")
    val readers = mutableListOf("first", "third")
    println(readers.filter { it !in authors })
}
複製程式碼

執行結果為:

>> [third]
複製程式碼

其中filter的定義為:

fun <T> List<T>.filter(predicate : (T) -> Boolean) : List<T> 
複製程式碼

在上面的例子中,自動生成的lambda引數it的型別為String。編譯器推斷T就是String,因為它知道函式是在List<T>上呼叫,而它的接收者readers的真實型別是List<String>

2.1.3 宣告泛型的擴充套件屬性

我們可以給 類或介面的方法、頂層函式、擴充套件函式和擴充套件屬性 宣告型別引數,在上面的例子中,型別引數用在了接收者和lambda引數上,下面我們再來看一個 宣告泛型的擴充套件屬性 的例子:

val <T> List<T>.penultimate: T
    get() = this[size - 2]

fun main(args: Array<String>) {
    println(listOf(1, 2, 3, 4).penultimate)
}
複製程式碼

執行結果為:

>> 3
複製程式碼

2.1.4 不能宣告泛型的非擴充套件屬性

普通(非擴充套件)屬性 不能擁有型別引數,不能在一個型別的屬性中儲存多個不同型別的值,因此 宣告泛型非擴充套件函式沒有任何意義

2.2 泛型類和泛型介面

Kotlin通過在類名稱後加上一對尖括號,並把型別引數放在尖括號內來宣告泛型類和泛型介面。一旦宣告後,就可以在型別的主體內 像其它型別一樣使用型別引數,例如List<T>

interface List<T> {
    operator fun get(index : Int) : T
}
複製程式碼

如果你的類繼承了泛型類,或者實現了泛型介面,就得 為基礎型別的泛型形參提供一個型別實參,它可以是一個 具體型別或者是另一個型別形參

2.2.1 將型別形參替換為具體型別

下面我們先定義一個泛型類Holder<T>,再將它的型別形參替換為具體型別Int

interface Holder<T> {
    fun getValue() : T
    fun setValue(t : T)
}

class HolderInt : Holder<Int> {
    var a : Int = 0;
    override fun getValue() = a
    override fun setValue(value : Int) {
        a = value
    }
}

fun main(args: Array<String>) {
    val t = HolderInt()
    t.setValue(2)
    println("value=${t.getValue()}")
}
複製程式碼

執行結果為:

>> value=2
複製程式碼

2.2.2 將型別形參替換為另一個型別形參

HolderWrapper定義了它自己的型別引數T並把它指定為父類的型別引數,它是全新的型別形參,不必保留一樣的名稱:

interface HolderWrapper<T> : Holder<T> 

class HolderInt : HolderWrapper<Int> {
    var a : Int = 0;
    override fun getValue() = a
    override fun setValue(value : Int) {
        a = value
    }
}
複製程式碼

2.2.3 類自身作為型別實參引用

一個類甚至可以把它自己作為型別實參引用,實現Comparable介面的類就是這種模式的經典例子,任何可以比較的元素都必須定義它如何與同樣型別的物件比較。

interface Comparable<T> {
    fun compareTo(other : T) : Int
}

class String : Comparable<String> {
    override fun compareTo(other : String) : Int = /** **/
}
複製程式碼

String類實現了Comparable泛型介面,提供型別String給型別實參T

2.3 型別引數約束

型別引數約束 可以限制作為 泛型類和泛型函式的型別實參的型別。例如計算列表元素之和的函式sum,它可以用在List<String>List<Double>上,但不可用在List<String>上,可以 定義一個型別引數約束,說明sum的型別形參必須是數字。

如果你把一個型別指定為泛型型別形參的上界約束,在泛型型別具體的初始化中,其對應的型別實參就必須是這個具體型別或者它的子型別。約束的定義方式為:把冒號放在型別引數名稱之後,作為型別形參上界的型別緊隨其後,例如:

fun <T : Number> List<T>.sum() : T
複製程式碼

如果需要在一個型別引數上指定多個約束,需要使用不同的語法:

fun <T> ensureTrailingPeriod(seq : T) 
        where T : CharSequence, T : Appendable {
        //...
}
複製程式碼

在這種情況下,作為型別實參的型別必須實現CharSequenceAppendable兩個介面。

2.4 讓型別實參非空

如果你宣告的是泛型類或者泛型函式,包括可空的型別實參在內,任何型別實參都可以替換它的型別形參。事實上,沒有指定上界的型別形參將會使用Any?作為預設的上界,關於Any的含義可以參考 Kotlin 知識梳理(7) - Kotlin 的型別系統 中的2.4節。

如果你想保證替換型別形參的始終是非空型別,可以通過指定一個約束來實現,如果除了可空性之外沒有任何限制,那麼可以使用Any代替預設的Any?作為上界。

class Processor<T : Any> {
    fun process(value : T) {
        value.hashCode()
    }
}
複製程式碼

除了Any之外,還可以指定任意非空型別作為上界,來讓型別引數非空。


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章