Kotlin 泛型

Kotlin 中也有泛型的概念,和 Java 中的類似,但又不盡相同,一起來認識 Kotlin 中的泛型吧。

一、基本使用

通常我們會在類、接口、方法中聲明泛型:

1、泛型類

class Animal<T> {}

2、泛型接口

interface IAnimal<T> {}

3、泛型方法

fun <T> initAnimal(param: T) {}

二、泛型約束

泛型約束表示我們可以指定泛型類型(T)的上界,即父類型,默認的上界爲Any?,如果只有一個上界可以這樣指定:

fun <T : Animal<T>> initAnimal(param: T) {}

Animal<T>就是上界類型,這裏使用了:,在 Java 中對應extends關鍵字,如果需要指定多個上界類型,就需要使用where語句:

fun <T> initAnimal(param: T) where T : Animal<T>, T : IAnimal<T> {}

三、類型擦除

Kotlin 爲泛型聲明執行的類型安全檢測僅在編譯期進行, 運行時實例不保留關於泛型類型的任何信息。這一點在 Java 中也是類似的。

例如,Array<String>Array<Int>的實例都會被擦除爲Array<*>,這樣帶來的好處是保存在內存中的類型信息也就減少了。

由於運行時泛型信息被擦除,所以在運行時無法檢測一個實例是否是帶有某個類型參數的泛型類型,所以下面的代碼是無法通過編譯的(Cannot check for instance of erased type: Array<Int>):

fun isArray(a: Any) {
    if (a is Array<Int>) {
        println("is array")
    }
}

但我們可以檢測一個實例是否是數組,雖然 Kotlin 不允許使用沒有指定類型參數的泛型類型,但可以使用星投影*(這個後邊會說到):

fun isArray(a: Any) {
    if (a is Array<*>) {
        println("is array")
    }
}

同樣原因,由於類型被擦除,我們也無法安全的將一個實現轉換成帶有某個類型參數的泛型類型:

fun sumArray(a: Array<*>) {
    val intArray = a as? Array<Int> ?: throw IllegalArgumentException("Array的泛型類型必須是Int類型")
    println(intArray.sum())
}

因爲我們無法判斷數組a的是不是Array<Int>類型的,所以可能會出現異常的情況。

對於泛型函數,如果在函數內需要使用具體的泛型類型,同樣由於運行時泛型信息被擦除的原因,你無法直接使用它(Cannot check for instance of erased type: T):

fun < T> test(param: Any) {
    if (param is T){
        println("param type is match")
    }
}

但還是有辦法的,可以用inline關鍵字修飾函數,即內聯函數,這樣編譯器會把每一次函數調用都換成函數實際代碼實現,同時用reified關鍵字修飾泛型類型,這樣就能保留泛型參數的具體類型了:

inline fun <reified T> test(param: Any) {
    if (param is T){
        println("param type is match")
    }
}

四、型變

1、聲明處型變

型變是泛型中比較重要的概念,首先我們要知道 Kotlin 中的泛型是不型變的,這點和 Java 類似。那什麼是型變呢,看個例子:

open class A
class B : A()

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = array1

你會發現第二個賦值語句會有錯誤提示,Type mismatch. Required:Array<A> Found:Array<B>類型不匹配,Array<B>並不是Array<A>的子類,就是因爲 Kotlin 中的泛型是默認不型變的,無法自動完成類型轉換,但BA的子類,這個賦值操作本質上是合理的、安全的,但編譯器似乎並不知道,這必然給我們開發過程中帶來了麻煩。

爲什麼Array無法正常的賦值,而ListSetMap可以呢?如下代碼,編譯器不會有錯誤提示的:

val list1: List<B> = listOf(B(), B(), B())
val list2: List<A> = list1

我們可以對比一下ArrayList在源碼中的定義:

public class Array<T> {}

public interface List<out E> : Collection<E> {}

可以看到List的泛型類型使用了out修飾符,這就是關鍵所在了。這就是 Kotlin 中的聲明處型變,用來向編譯器解釋這種情況。

  • 關於out修飾符我們可這樣理解,當類、接口的泛型類型參數被聲明爲out時,則該類型參數是協變的,泛型類型的子類型是被保留的,它只能出現在函數的輸出位置,只能作爲返回類型,即生產者。帶來的好處是,AB的父類,那麼List<A>可以是List<B>的父類。

我們修改下上邊List賦值的代碼:

val list1: List<A> = listOf(A(), A(), A())
val list2: List<B> = list1

即反過來賦值,由於B並不是A的父類,會有Type mismatch. Required:List<B> Found:List<A>錯誤提示。爲了應對這種情況,Kotlin 還提供了一個in修飾符。

  • 關於in修飾符我們可這樣理解,當類、接口的泛型類型參數被聲明爲in時,則該類型參數是逆變的,泛型類型的父類型是被保留的,它只能出現在函數的輸入位置,作爲參數,只能作爲消費類型,即消費者

其實 Kotlin 中的Comparable接口使用了in修飾符:

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

寫一個測試函數,編譯器並不會報錯:

fun test(a: Comparable<A>) {
    val b: Comparable<B> = a
}

所以in修飾符和out修飾符的作用看起來的相對的,AB的父類,那麼Comparable<B>可以是Comparable<A>的父類,體會下區別。

2、使用處型變

爲了能將Array<B>賦值給Array<A>,我們修改下之前的代碼:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<out A> = array1

這就是使用處型變,相比聲明處型變,使用處型變就要複雜些,爲了完成對應的需求,需要每次使用對應類時都添加型變修飾符。而聲明處型變在類、接口聲明時就做好了這些工作,因而代碼會更加簡潔。

再看一個數組拷貝的函數:

fun copy(from: Array<A>, to: Array<A>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

我們試着執行如下的拷貝操作:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = arrayOf(A(), A(), A())
copy(array1, array2)

同樣的問題,由於泛型默認不型變的原因,copy(array1, array2)並不能正常工作。

回想一下,在 Java 中類似的問題可以使用通配符類型參數解決這個問題:

public void copy(ArrayList<? extends A> from, ArrayList<? super A> to) {}

那麼在 Kotlin 中我們自然想到的是型變修飾符了:

  • Kotlin 中的out A類似於 Java 中的? extends A,即泛型參數類型必須是A或者A子類,用來確定類型的上限
  • Kotlin 中的in A類似於 Java 中的? super A,即泛型參數類型必須是B或者B父類,用來確定類型的下限

修改上邊的 copy函數:

fun copy(from: List<out A>, to: List<in A>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

這樣copy函數就能正常的工作了。使用處型變其實也是一種類型投影fromin此時都是一個類型受限的投影數組,它們只能返回、接收指定類型的數據。

這些概念很容易把人搞暈,理解其作用纔是關鍵,而不是套概念。

五、類型投影

前邊我們已經知道使用處型變也是一種類型投影,除此之外還有一種星投影

當我們不知道泛型參數的類型信息時,但仍需要安全的使用它時,可以使用星投影,用星號*表示,星投影和 Java 中的原始類型很像,但星投影是安全。

官方對星投影語法的解釋如下:

  • 對於 Foo <out T : TUpper>,其中 T 是一個具有上界 TUpper 的協變類型參數,Foo <> 等價於 Foo <out TUpper>。 這意味着當 T 未知時,你可以安全地從 Foo <> 讀取 TUpper 的值。
  • 對於 Foo <in T>,其中 T 是一個逆變類型參數,Foo <> 等價於 Foo <in Nothing>。 這意味着當 T 未知時,沒有什麼可以以安全的方式寫入 Foo <>。
  • 對於 Foo <T : TUpper>,其中 T 是一個具有上界 TUpper 的不型變類型參數,Foo<*> 對於讀取值時等價於 Foo<out TUpper> 而對於寫值時等價於 Foo<in Nothing>。

如果泛型類型具有多個類型參數,則每個類型參數都可以單獨投影。 例如,如果類型被聲明爲 interface Function <in T, out U>,我們可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>;
  • Function<Int, *> 表示 Function<Int, out Any?>;
  • Function<*, *> 表示 Function<in Nothing, out Any?>

我們來看如下的代碼:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<*> = array1

使用星投影,我們可以將array1賦值給array2,但由於此時array2並不知道泛型參數的類型,所以不能對array2進行數據寫入的操作,但可以從中讀取數據:

array2[0] =A() //編譯器會報錯
val a = array2[0] // 正常

可以看出,星投影更適合那些泛型參數的類型不重要的場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章