1. 程式人生 > >Kotlin自學之旅(十)泛型

Kotlin自學之旅(十)泛型

文章目錄

宣告泛型函式

泛型允許你定義帶型別形參的型別。當這種型別的例項被創建出來的時候,型別形參被替換成稱為型別實參的具體型別。Kotlin中泛型的使用與宣告和Java很相似,我們宣告一個泛型函式,可以像下面這麼寫:

fun <T> sayHello(item : T) {  //普通函式
    println(" ${item.toString()} say hello")
}

fun <T> T.sayHi() {   //擴充套件函式
    println("${toString()} say hi"
) }

型別引數T的宣告在關鍵字 fun 和函式名稱之間。使用泛型函式時,我們可以顯式地指定型別實參,編譯器可以推匯出來時也可以省略:

val xiaoMing = "xiaoming"
   
xiaoMing.sayHi<String>()
sayHello(xiaoMing)

宣告泛型類

Kotlin 通過在類名稱後加上一對尖括號 ,井把型別引數放在尖括號內來宣告泛型類及泛型介面。一旦宣告之後,就可以在類的主體內像其他型別一樣使用型別引數:

interface Speakable<T> {
   fun sayHello(item: T)
}

class Person<
T>(var name: T) :Speakable<T>{ override fun sayHello(item: T) { println("$name say hello to $item") } } val xiaoming = Person("xiaoming") xiaoming.sayHello("xiaogang") //輸出:xiaoming say hello to xiaogang

使用泛型類時如果編譯器可以推斷出型別引數那麼可以省略型別實參。實現泛型介面時需要為它的形參提供一個型別實參(在上面的例子中,是Person用自己的型別形參作為Speakable介面的實參)。 #型別引數約束 型別引數約束可以限制作為(泛型)類和(泛型)函式的型別實參的型別。如果把一個型別指定為泛型型別形參的上界約束,在泛型型別具體的初始化中,其對應的型別實參就必須是這個具體型別或者它的子型別。泛型約束的語法如下:

fun <T:Number> half(item :T):Double {
    return item.toDouble() / 2
}

println(half(3)) //1.5

一旦指定了型別形參 T 的上界,你就可以把型別 T 的值當作它的上界(型別)的值使用。例如,可以呼叫定義在上界類中的方法。而當沒有指定型別引數的上界時,它的上界預設為Any?。

型變

型變的概念描述了擁有相同基礎型別和不同型別實參的(泛型)型別之間是如何關聯的:例如, List<String>和 List<Any>之間如何關聯。為什麼我們需要型變呢?因為型變的存在,很大的提高了泛型API的靈活性。

型變的概念

為了討論型別之間的關係,需要熟悉子型別這個術語。任何時候如果需要的是型別 A 的值,你都能夠使用型別 B 的值(當作 A 的值),型別 B 就稱為型別 A 的子型別。術語超型別是子型別的反義詞。如果 A 是 B 的子型別,那麼 B 就是 A 的超型別。知道一個類是否是另一個類的子型別是十分重要的,因為編譯器在每一次給變數賦值或者給函式傳遞實參的時候都要做一項檢查,只有值的型別是變數型別的子型別時,才允許變數儲存該值。因此我們可以將子型別作為超型別來使用,這就是型變能提高API靈活性的原因。 簡單的情況下,子型別和子類本質上意味著一樣的事物。例如,Int 類是 Number 的子類,因此 Int 型別是 Number 型別的子型別。如果一個類實現了一個介面,它的型別就是該介面型別的子型別:String 是 CharSequence 的子型別 。 那麼對於型別引數分別為子型別和超型別的兩個泛型型別,比如 List<Int> 和 List<Number> 來說,情況是怎麼樣的呢?顯然,它們只有三種可能的關係:

  • List<Int> 是 List<Number> 的子型別,這種情況我們稱之為協變
  • List<Int> 是 List<Number> 的超型別。這種情況我們稱之為逆變
  • List<Int> 和 List<Number> 沒有關係。這種情況我們稱之為不變

不變

首先來看最簡單的不變情況:List<Int> 和 List<Number>是兩個不相關的類,這不會隱藏什麼危險,但是我們來看下面一個情形:

open class Animal {
    var weight: Int = 0
    fun feed() {}
}
class Cat : Animal() {
	fun catchMouse() {}
}

fun feedAll(animal: List<Animal>) {
    for (i in 0 until animal.size) {
        animal[i].feed()
    }
}
val cats = listOf(Cat(),Cat())
// feedAll(cats)  //error

當我們將List <Cat> 作為引數使用feeaAll()時,將會報錯(假設 List是不變的),因為List<Animal> 和 List<Cat>沒有關係(當然我們可以使用強制型別轉換,但這種方式有很大的風險),於是我們只能再宣告一個feedAll(cats: List<Cat>),以及為更多的可能出現的Dog、Hamster、Tortoise…等宣告方法,這顯然是種很浪費的情況。

協變

如果List是協變的,那麼這種難題就迎刃而解了,既然 List<Cat> 是 List<Animal> 的子型別,那麼引數型別為 List<Animal> 的地方完全可以使用 List<Cat>。但是這個會有什麼風險呢?看下面這種情形:

class Dog : Animal()

fun walk(animal: List<Animal>) {
    animal.add(Dog())
}

walk(cats)
for (cat in cats) {
    cat.catchMouse() // error: dog can't catch mouse
}

我們想帶著一群貓出去散步,我們有一個帶著一群動物的出去散步的方法,貓是動物,於是通過協變,我們成功帶著這群貓去散步了。但是在散步的路上,一隻狗混了進來(假設List有add方法),狗當然也是動物,於是狗安然無恙地跟著我們散完步回家了,回到家後這群貓就要去抓老鼠,這時候這隻狗就混不下去了,因為它並沒有抓老鼠的方法。於是程式就出錯了。 我們可以看出,問題出在我們讓狗混進了貓群,換言之,當我們把一個子類類參的集合通過協變轉換成父類類參的集合之後,有將其他子類放入集合的危險。而只要避免這種情況:我們只使用集合,而不向集合中新增元素,從而不改變這個集合中擁有的元素型別,那麼這種行為就是安全的。

逆變

最後是逆變,初看這個概念可能會有點奇怪,因為型別引數的子型別關係被反轉了。但是逆變也是很有用武之地的。來看下面這種情況:我們有兩隻貓,我們想知道哪隻貓更重,我們當然還記得曹衝稱象的故事:

class Elephant: Animal()

fun compare(elephant1: Elephant,elephant2: Elephant):Int {
    return boat(elephant1) - boat(elephant2)
}

fun boat(elephant: Elephant): Int {
    return depthOfWater()
}

雖然曹衝稱的是象,但把象換成貓情況也是一樣的:於是我們找了一艘船,分別把貓放上去,再比較船吃水的深度,誰輕誰重就一目瞭然了。事實上,對於任何動物,這個方法都是OK的:

interface Comparator<T> {
    fun compare(e1: T,e2: T):Int
}

class BoatComparator: Comparator<Animal> {
    override fun compare(e1: Animal, e2: Animal): Int {
        return boat(e1) - boat(e2)
    }

    private fun boat(e:Animal):Int {
        return depthOfWater()
    }
}

這樣看起來很好,當我們想用這個方法來找到一群貓中那隻最重的貓的時候,我們理所當然的會想到使用這個辦法:

fun whoIsHeaviestCat(list: List<Cat>, comparator: Comparator<Cat>): Int{
    var index = 0
    for (i in 1..list.size) {
        if (comparator.compare(list[index],list[i]) >0) {
            index = i
        }
    }
    return index
}
val list = listOf(Cat(),Cat(),Cat())
whoIsHeaviestCat(list, BoatComparator()) //error

但是當我們使用BoatComparator的時候,方法報錯了,因為方法引數是Comparator<Cat>,而我們使用的是Comparator<Animal>,雖然Comparator<Animal>顯然也可以用來比較貓。怎麼辦呢,如果我們再宣告一個BoatComparator: Comparator<Cat>,這又是之前協變時的問題:太浪費了。於是這時候逆變就派上用場了:如果Comparator<T>是逆變的,那麼Comparator<Animal>就是 Comparator<Cat> 的子型別,那麼 whoIsHeaviest(list,BoatComparator()) 就不會報錯了。 同樣的,逆變又可能導致什麼問題呢?來想象下面這種情況:出於某種想法,我們想修改一下Comparator的API:把compare函式的返回值改為e1、e2中較大的那一個,顯然這時返回型別就是T:

interface Comparator<T> {
    fun compare(e1: T,e2: T):T
}

於是BoatComparator的返回值也需要改變成Animal,到此為止還沒什麼問題,但是在whoIsHeaviestCat中我們再呼叫compare時,得到的返回值就也變成了Animal——型別資訊在逆變中失去了,這個時候我們當然知道它是一隻Cat,可以使用強制型別轉換回來,但前面也說了,這不是一種好的程式碼風格。 於是我們知道了,逆變的風險在於丟失型別資訊,而如果我們保證沒有在逆變的地方將這個值儲存或者傳遞,那麼就沒有變數持有這個值,也就沒有丟失型別資訊的問題了。

小結

綜上所述,我們為了介面的靈活和安全,應該有限制的使用形變,我們應該對協變的物件只讀取不寫入,而對逆變的物件只寫入不讀取:第一種物件被稱為生產者,第二種物件被稱為消費者。 在函式或者方法宣告中型別引數的使用可以分為 in 位置和 out 位置。考慮這樣一個類,它聲明瞭一個型別引數 T 幷包含了一個使用 T 的函式。如果函式是把 T 當成返回型別,我們說它在 out 位置。這種情況下,該函式生產型別為 T 的值 。 如果 T 用作函式引數的型別,它就在 in 位置。 在這裡插入圖片描述 所以對於生產者,它只應該被讀取,所以只能在用於 out 的位置;而對於消費者,它只應該被寫入,所以只能在 in 的位置。這也就是Kotlin宣告泛型的規則。

宣告處型變

在 Kotlin 中,我們可以使用 in 或 out 修飾符標註泛型類的型別引數來確保它僅從類成員中消費或者生產:當一個類 C 的型別引數 T 被宣告為 out 時,它就只能出現在 C 的成員的 out 位置,但回報是 C 可以安全地作為 C的超類。即類 C 是在引數 T 上是協變的,或者說 T 是一個協變的型別引數。 也就是說 C 是 T 的生產者,而不是 T 的消費者:

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 這個沒問題,因為 T 是一個 out-引數
    // ……
}

當一個類 C 的型別引數 T 被宣告為 in 時,它使得一個型別引數逆變,它就只能出現在 C 的成員的 in 位置,只可以被消費而不可以被生產:

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

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 擁有型別 Double,它是 Number 的子型別
    // 因此,我們可以將 x 賦給型別為 Comparable <Double> 的變數
    val y: Comparable<Double> = x // OK!
}

使用處型變

有些類既不是協變也不是逆變的,因為它同時生產和消費指定為它們型別引數的型別的值 。 但是對於這個型別的變數來說,在某個特定函式中只被當成生產者和消費者其中一種角色使用。對於這種情況, Kotlin 提供了 的解決方案就是使用處型變:當函式的實現呼叫了那些型別引數只出現在 out 位置(或只出現在 in位置)的方法時,可以在函式定義中給特定用途的型別引數加上變型修飾符,從而在這個函式內,這個型別引數的逆變或協變是安全的:

fun <T> copyData(source: MutableList<T>,
                 destination: MutableList<in T>) {
    for (item in source) {
        destination.add(item)
    }
}

val ints: MutableList<Int> = mutableListOf(1, 2, 3)
val any: MutableList<Any> = mutableListOf()
copyData(ints,any)

可以為型別宣告中型別引數任意的用法指定變型修飾符,這些用法包括:形參型別、區域性變數型別、函式返回型別等等。這種方式也被叫做型別投影,因為在這個函式內,修飾符修飾的函式引數的使用被限制了。

星投影

如果你不知道關於泛型實參的任何資訊,你可以使用星號表示為C<*>。 * 和 Any? 是不同的,比如 MutableList <Any?>這種列表包含的是任意型別的元素。而另一方面,MutableList <*>是包含某種特定型別元素的列表,但是你不知道是哪個型別 。因為不知道*是哪個型別,你不能向列表中寫入任何東西,因為你寫入的任何值都可能會違反呼叫程式碼的期望。但是從列表中讀取元素是可行的,因為列表會返回所有 Kotlin 型別的超型別 Any? 。 編譯器會把 MutableList <*>當成 out 投影的型別,在上面這個例子中,MutableList <*>投影成了 MutableList<out Any?>:當你沒有任何元素型別資訊的時候,讀取 Any?型別的元素仍然是安全的,但是向列表中寫入元素是不安全的 。所以星號投影的語法很簡沽,但只能用在對泛型型別實參的確切值不感興趣的地方:只是使用生產值的方法,而且不關心那些值的型別。

總結

本文介紹了Kotlin中泛型的宣告和使用方式,並簡單介紹了變型的概念和在Kotlin的實現。總的來說,Kotlin中的泛型和Java中的大同小異:使用處型變的 out T 相當於 Java 中的 ? extends Tin T 相當於 Java 中的 ? super T;宣告處型變是使用處型變的簡寫方式;星投影 MyType<*> 對應於 Java 的 MyType<?>