亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Kotlin 泛型型變

上篇文章我們一起為 Kotlin 中的泛型型變做了一個很好的鋪墊,深入分析下類型和類,子類型和子類之間的關系、什么是子類型化關系以及型變存在的意義。那么今天將會講點更有挑戰性的東西,也就是 Kotlin 泛型型變中最為難理解的地方,那就是 Kotlin 中的協變、逆變、不變。

1. 泛型協變 (保留子類型化關系)

1.1 協變基本定義和介紹

還記得上篇的子類型化關系嗎?協變實際上就是保留子類型化關系,首先我們需要去明確一下這里所說的保留子類型化關系是針對誰而言的呢?

基本介紹

來看個例子,StringString? 的子類型,我們知道基礎類型 List<out E> 是協變的,那么 List<String> 也就是 List<String?> 的子類型的。很明顯這里針對的角色就是 List<String>List<String?>, 是它們保留了 StringString? 的子類型化關系?;蛘邠Q句話說兩個具有相同的基礎類型的泛型協變類型,如果類型實參具有子類型化關系,那么這個泛型類型具有一致方向的子類型化關系。那么具有子類型化關系實際上子類型的值能在任何時候任何地方替代超類型的值。

基本定義

interface Producer<out T> {//在泛型類型形參前面指定out修飾符
   val something: T
   fun produce(): T
}

1.2 什么是 out 協變點

從上面定義的基本結構來看,實際上協變點就是上面 produce 函數返回值的 T 的位置,Kotlin 中規定一個泛型協變類,在泛型形參前面加上 out 修飾后,那么修飾這個泛型形參在函數內部使用范圍將受到限制只能作為函數的返回值或者修飾只讀權限的屬性。

interface Producer<out T> {//在泛型類型形參前面指定out修飾符
   val something: T//T作為只讀屬性的類型,這里T的位置也是out協變點
   fun produce(): T//T作為函數的返回值輸出給外部,這里T的位置就是out協變點
}

以上協變點都是標準的 T 類型,實際上以下這種方式其實也是協變點,請注意體會協變點含義:

interface Producer<out T> {
   val something: List<T>//即使T不是單個的類型,但是它作為一個泛型類型修飾只讀屬性,所以它所處位置還是out協變點
   
   fun produce(): List<Map<String,T>>//即使T不是單個的類型,但是它作為泛型類型的類型實參修飾返回值,所以它所處位置還是out協變點
}

1.3 out 協變點基本特征

協變點基本特征: 如果一個泛型類聲明成協變的,用 out 修飾的那個類型形參,在函數內部出現的位置只能在只讀屬性的類型或者函數的返回值類型。相對于外部而言協變是生產泛型參數的角色,生產者向外輸出 out。

1.4 協變 -List<out E> 的源碼分析

我們在上篇文章中就說過 Kotlin 中的 List 并不是 Java 中的 List, 因為 Kotlin 中的 List 是個只讀的 List 不具備修改集合中元素的操作方法。Java 的 List 實際上相當于 Kotlin 中的 MutableList 具有各種讀和寫的操作方法。

Kotlin 中的 List<out E> 實際上就是協變的例子,用它來說明分析協變最好不過了,還記得上篇文章說過的學習泛型步驟二嗎,就是通過分析源碼來驗證自己的理解和結論。通過以下源碼均可驗證我們上述所說的結論。

//通過泛型類定義可以看出使用out修飾符 修飾泛型類型形參E
public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean//咦! 咦! 咦! 和說的不一樣啊,為什么還能出現在這個位置,還出來了個@UnsafeVariance 這個是什么鬼? 告訴你,穩住,先不要急,請聽我在后面慢慢說來,先暫時保留神秘感
    override fun iterator(): Iterator<E>//這里明顯能看出來E處于out協變點位置,而且還是泛型類型Iterator<E>出現的,正好驗證我們上述所說的協變的變種類型(E為類型實參的泛型類型)

    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E//函數返回值的類型E,這里明顯能看出來E處于out協變點位置,正好驗證我們上述所說的協變的標準類型(E直接為返回值的類型)
    public fun indexOf(element: @UnsafeVariance E): Int

    public fun lastIndexOf(element: @UnsafeVariance E): Int

    public fun listIterator(): ListIterator<E>//(E為類型實參的泛型類型),為out協變點

    public fun listIterator(index: Int): ListIterator<E>//(E為類型實參的泛型類型),為out協變點
    public fun subList(fromIndex: Int, toIndex: Int): List<E>//(E為類型實參的泛型類型),為out協變點
}

源碼分析完了,是不是感覺還是有點迷惑啊?就是 E 為啥還能在其他的位置上,還有 @UnsafeVariance 是個什么東西呢?這些疑問先放一放,但是上述至少證明了泛型協變 out 協變的位置是返回值的類型以及只讀屬性的類型 (這點源碼中沒有表現出來,但是實際上卻是如此啊,這里可以自行查閱其他例子)。

2. 泛型逆變 (反轉子類型化關系)

2.1 逆變基本定義和介紹

基本介紹

逆變實際上就是和協變子類型化關系正好相反,它是反轉子類型化關系。

來個例子說明下,我們知道 StringString? 的子類型,Comparable<in T> 是逆變的,那么 Comparable<String>Comparable<String?> 實際上是反轉了 StringString? 的子類型化關系,也就是和 StringString? 的子類型化關系相反,那么 Comparable<String?> 就是 Comparable<String> 子類型,Comparable<String> 類型值出現的地方都可用 Comparable<String?> 類型值來替代。

換句話說就是: 兩個具有相同的基礎類型的泛型逆變類型,如果類型實參具有子類型化關系,那么這個泛型類型具有相反方向的子類型化關系

  • 基本定義
interface Consumer<in T>{//在泛型類型形參前面指定in修飾符
   fun consume(value: T)
}

2.2 什么是 in 逆變點

從上面定義的基本結構來看,實際上逆變點就是上面 consume 函數接收函數形參的 T 的位置,Kotlin 中規定一個泛型協變類,在泛型形參前面加上 out 修飾后,那么修飾這個泛型形參在函數內部使用范圍將受到限制只能作為函數的返回值或者修飾只讀權限的屬性。

interface Consumer<in T>{//在泛型類型形參前面指定in修飾符
   var something: T //T作為可變屬性的類型,這里T的位置也是in逆變點
   fun consume(value: T)//T作為函數形參類型,這里T的位置也就是in逆變點
}

和協變類似,逆變也存在那種泛型類型處于逆變點的位置,這些我們都可以把當做逆變點:

interface Consumer<in T>{
   var something: B<T>//這里雖然是泛型類型但是T所在位置依然是修飾可變屬性類型,所以仍處于逆變點
   fun consume(value: A<T>)//這里雖然是泛型類型但是T所在位置依然是函數形參類型,所以仍處于逆變點
}

2.3 in 逆變點基本特征

逆變點基本特征: 如果一個泛型類聲明成逆變的,用 in 修飾泛型類的類型形參,在函數內部出現的位置只能是作為可變屬性的類型或者函數的形參類型。相對于外部而言逆變是消費泛型參數的角色,消費者請求外部輸入 in。

2.4 逆變 -Comparable<in T> 的源碼分析

在 Kotlin 中其實最簡單的泛型逆變的例子就是 Comparable<in T>

public interface Comparable<in T> {//泛型逆變使用in關鍵字修飾
    /**
     * Compares this object with the specified object for order. Returns zero if this object is equal
     * to the specified [other] object, a negative number if it's less than [other], or a positive number
     * if it's greater than [other].
     */
    public operator fun compareTo(other: T): Int//因為是逆變的,所以T在函數內部出現的位置作為compareTo函數的形參類型,可以看出它是屬于消費泛型參數的
}

3. 泛型不變 - 無子類型化關系

3.1 不變基本定義和介紹

基本介紹

對于不變就更簡單了,泛型型變中除去協變、逆變就是不變了。其實不變看起來就是我們常用的普通泛型,它既沒有 in 關鍵字修飾,也沒有 out 關鍵字修飾。它就是普通的泛型,所以很明顯它沒有像協變、逆變那樣那么多的條條框框,它很自由既可讀又可寫,既可以作為函數的返回值類型也可以作為函數形參類型,既可以聲明成只讀屬性的類型又可以聲明可變屬性。

但是注意了:不變型就是沒有子類型化關系,所以它會有一個局限性就是如果以它作為函數形參類型,外部傳入只能是和它相同的類型,因為它根本就不存在子類型化關系說法,那也就是沒有任何類型值能夠替換它,除了它自己本身的類型 例如 MutableList<String>和MutableList<String?> 是完全兩種不一樣的類型,盡管 StringString? 子類型,但是基礎泛型 MutableList<E> 是不變型的,所以 MutableList<String>和MutableList<String?> 根本沒關系。

基本定義


interface MutableList<E>{//沒有in和out修飾
   fun add(element: E)//E可以作為函數形參類型處于逆變點,輸入消費E
   fun subList(fromIndex: Int, toIndex: Int): MutableList<E>//E又可以作為函數返回值類型處于協變點,生產輸出E
}

4. 協變、逆變、不變的規則引出幾個問題

思考 1:協變泛型類的泛型形參類型 T 一定就只能 out 協變點位置嗎?能不能在 in 逆變點位置呢?

解答 1:可以在逆變點,但是必須在函數內部保證該泛型參數 T 不存在寫操作行為,只能有讀操作
出現的場景:

聲明了協變的泛型類,但是有時候需要從外部傳入一個該類型形參的函數參數,那么這個形參類型就處于 in 逆變點的位置了,但是函數內部能夠保證不會對泛型參數存在寫操作的行為。常見例子就是 List<out E> 源碼,就是上面大家一臉懵逼的地方,就是那個為什么定義成協變的泛型 T 跑到了函數形參類型上去。 如下面部分代碼所示:

  override fun contains(element: @UnsafeVariance E): Boolean//咦! 咦! 咦! 和說的不一樣啊,為什么還能出現在這個位置,還出來了個@UnsafeVariance 這個是什么鬼? 現在回答你就是可能會出現在這,但是只要保證函數不會寫操作即可

上述的 List 中的 contains 函數形參就是泛型形參 E,它是協變的出現在逆變點,但是只要保證函數內部不會對它有寫操作即可。

思考 2:逆變泛型類的泛型形參類型 T 就一定只能在 in 逆變點位置嗎?能不能在 out 協變點位置呢?
解答 2:同理,也可以在協變點位置。

思答 3:能在其他的位置嗎?比如構造函數?
解答 3:可以在構造器函數中,因為這是個比較特殊的位置,既不在 in 位置也不在 out 位置。


class ClassMates<out T: Student>(vararg students: T){//可以看到雖然定義成了協變,但是這里的T不是在out協變點的位置,這種聲明依然是合法的
   ...
}

注意: 這里就是很特殊的場景了,所以開頭就說過了如果把這些規則,用法只是死記硬背下來,碰到這種場景的時候就開始懷疑人生了,規則中不是這樣的啊,規則中定義協變點就是只讀屬性類型和函數返回值類型的位置啊,這個位置不上不下的該怎么解釋呢?

所以解決問題還是需要抓住問題的關鍵才是最主要的。其實解釋這個問題也不難,回到型變的目的和初衷上去,型變是為了解決類型安全問題,是防止更加泛化的實例調用某些存在危險操作的方法。構造函數很特殊一般創建后實例對象后,在該對象基礎上構造函數是不能再被調用的,所以這里 T 放在這里是安全的。

思考 4:為了安全,我是不是只要把所有泛型類全都定義成協變或逆變或不變一種就可以了呢?

解答 4:不行,這樣不安全,按照實際場景需求出發,一味定義成協變或逆變實際上限制了該泛型類對該類型形參使用的可能性,因為 out 只能是作為生產者,協變點位置有限制,而 in 只能是消費者逆變點的位置也有限制。

那索性全都定義成不變型,那就在另一層面喪失了靈活性,就是它失去了子類型化關系,就是把它作為函數參數類型,外部只能傳入和它相同的類型,不可能存在子類型化關系的保留和反轉了

5. 協變點、逆變點的本質

由上面的思考明白了一點,使用協變、逆變的時候并不是那么死的按照協變點,逆變點規則來,可以更加靈活點,關鍵是不能違背協變、逆變根本宗旨。協變宗旨就是定義的泛型類內部不能存在寫操作的行為,對于逆變根本宗旨一般都是只寫的。

那 Kotlin 中 List<out E> 的源碼來說都不是真正規則上說的那樣協變,泛型形參 E 并不都是在協變點 out 上,但是 List<out E> 內部能夠保證不會存在寫操作危險行為所以這種定義也是合法。實際上真正開發過程,很難做到協變泛型類中的泛型類型形參都是在 out 協變點上,因為有時候需求需要確實需要從外部傳入一個該類型形參的一個函數形參。

所以最終的結論是: 協變點 out 和逆變點 in 的位置的規則是一般大體情況下要遵守的,但是需要具體情況具體分析,針對設計的泛型類具體情況,適當地在不違背根本宗旨以及滿足需求情況下變下協變點和逆變點的位置規則

6. UnSafeVariance 注解在開發中的應用

由上面的本質區別分析,嚴格按照協變點、逆變點規則來是不能完全滿足我們真實開發需求場景的,所以有時候需要一道后門,那就要用特殊方式告訴它。那就是使用 UnSafeVariance 注解。所以 UnSafeVariance 注解作用很簡單: 通過 @UnSafeVariance 告訴編譯器該處安全性自己能夠把控,讓它放你編譯通過即可,如果不加編譯器認為這是不合法的

注解的意思就是不安全的型變,例如在協變泛型類中有個函數是以傳入一個該泛型形參的函數形參的,通過 UnSafeVariance 注解讓編譯器閉嘴,然后把它放置在逆變點實際上是增加一層危險性,相當于把這層危險交給了開發者,只要開發者能保證內部不存在危險性操作肯定就是安全的。

7. 協變、逆變、不變對比分析總結

7.1 分析對比

將從基本結構形式、有無子類型化關系 (保留、反轉)、有無型變點 (協變點 out、逆變點 in)、角色 (生產者輸出、消費者輸入)、類型形參存在的位置 (協變就是修飾只讀屬性和函數返回值類型;逆變就是修飾可變屬性和函數形參類型)、表現特征 (只讀、可寫、可讀可寫) 等方面進行對比

協變 逆變 不變
基本結構 Producer<out E> Consumer<in T> MutableList<T>
子類型化關系 保留子類型化關系 反轉子類型化關系 無子類型化關系
有無型變點 協變點 out 逆變點 in 無型變點
類型形參存在的位置 修飾只讀屬性類型和函數返回值類型 修飾可變屬性類型和函數形參類型 都可以,沒有約束
角色 生產者輸出為泛型形參類型 消費者輸入為泛型形參類型 既是生產者也是消費者
表現特征 內部操作只讀 內部操作只寫 內部操作可讀可寫

7.2 使用對比

實際上就是要明確什么時候該使用協變、什么時候該使用逆變、什么時候該使用不變。
實際上通過上述分析對比的表格可以得出結論:

首先,表格有很多個條件特征,到底是先哪個開始判定條件好呢?實際上這里面還是需要選擇一下的。

假設 1: 就比如一開始就以有無使用子類型化關系為條件做判定,這樣做法是有點問題的,試想下在實際開發中,先是去定義泛型類內部一些方法和屬性的,這時候很難知道在外部使用情況下存不存在利用子類型化關系,也就是存不存在用子類型的值替換超類型的值場景,所以在剛剛定義泛型類的時候很難明確的。故還是先從泛型類定義的內部特征著手會更加明確點。

假設 2:比如先根據泛型類內部定義一些方法和屬性,由于剛開始定義并不能確定是否是協變 out 還是逆變 in,所以上面的有無型變點不能作為判定條件,最開始還沒確定的時候一般當做不變泛型類來定義。最直白可以先看看型變點,然后根據型變點基本確定泛型類內部表現特征:

  • 步驟 1:首先,根據類型形參存在的位置初步判定;
  • 步驟 2:然后,通過判定表現特征是在泛型類定義內部是不是只涉及到該泛型形參只讀操作 (協變或不變),還是寫操作 (逆變或不變),還是既可讀又可寫 (不變) 這里只能判斷出兩種組合情況 (協變或不變)、(逆變或不變) 中的一種,因為如果只涉及到讀操作那就是 (協變或不變),如果只涉及寫操作 (逆變或不變)
  • 步驟 3:最后,再去看是否存在子類型化關系,如果通過步驟 2 得到是 (協變或不變) 外加有子類型化關系最終得到使用協變,如果通過步驟 2 得到是 (逆變或不變) 外加有子類型化關系最終得到使用逆變,如果沒有子類型化關系就用不變。

補充一點,如果最終確定是協變的,可是在定義的時候通過步驟 1 得到類型形參存在的位置處于函數形參位置,那么這時候就可以大膽借助 @UnSafeVariance 注解告訴編譯器使得編譯通過,逆變同理。
來張圖理解下:

圖片描述

7.3 理解對比

是否還記得上一篇文章開頭的那個例子和那幅漫畫圖:

對于協變的理解,例子代碼如下:

fun main(args: Array<String>) {
    val stringList: List<String> = listOf("a", "b", "c", "d")
    val intList: List<Int> = listOf(1, 2, 3, 4)
    printList(stringList)//向函數傳遞一個List<String>函數實參,也就是這里List<String>是可以替換List<Any>
    printList(intList)//向函數傳遞一個List<Int>函數實參,也就是這里List<Int>是可以替換List<Any>
}

fun printList(list: List<Any>) {
//注意:List是協變的,這里函數形參類型是List<Any>,函數內部是不知道外部傳入是List<Int>還是List<String>,全部當做List<Any>處理
    list.forEach {
        println(it)
    }
}

理解:對于 printList 函數而言,它需要的是 List<Any> 類型是個相對具體類型更加泛化的類型,且在函數內部的操作不會涉及到修改寫操作,然后在外部傳入一個更為具體的子類型肯定是滿足要求的泛化類型最基本需求。所以外部傳入更為具體子類型 List<String>、List<Int> 的兼容性更好。

對于逆變的理解,例子代碼如下:

class A<in T>{
    fun doAction(t: T){
        ...
    }
}

fun main(args: Array<String>) {

    val intA = A<Int>()
    val anyA = A<Any>()

    doSomething(intA)//不合法,
    doSomething(anyA)//合法
}

fun doSomething(a: A<Number>){//在doSomething外部不能傳入比A<Number>更為具體的類型,因為在函數內部涉及寫操作.
    ....
}

理解:對于 doSomething,它需要的 A<Number> 是個相對泛化類型更加具體的類型,由于泛型類 A 逆變的,函數內部的操作放開寫操作權限,試著想下在 doSomething 函數外部不能傳入比他更為具體的比較器對象了,因為只要有比 A<Number> 更為具體的,就會出問題,利用反證法來理解下,假如傳入 A<Int> 類型是合法的,那么在內部函數還是當做 A<Number>, 在函數內部寫操作時候很有可能把它往里面寫入一個 Float 類型的數據,因為往 Number 類型寫入 Float 類型是很合法的,但是外部實際上傳入的是 A<Int>,往 A<Int> 寫 Float 類型不出問題才怪呢,所以原假設不成立。所以逆變放開了寫權限,那么對于外部傳入的類型要求就更加嚴格了。

引出另一個問題,為什么逆變寫操作是安全的呢? 細想也是很簡單的,對于逆變泛型類型作為函數形參的類型,那么在函數外部的傳入實參類型就一定要比函數形參的類型更泛化不能更具體,所以在函數內部操作的最具體的類型也就是函數形參類型,所以肯定可以大膽寫操作啊。就比如 A<Number> 類型形參類型,在 doSomething 函數中明確知道外部不能比它更為具體,所以在函數內部大膽在 A<Number> 基礎上寫操作是可以的。

對于不變的理解,例子代碼如下:

fun main(args: Array<String>) {
    val stringList: MutableList<String> = mutableListOf("a", "b", "c", "d")
    val intList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
    printList(stringList)//這里實際上是編譯不通過的
    printList(intList)//這里實際上是編譯不通過的
}

fun printList(list: MutableList<Any>) {
    list.add(3.0f)//開始引入危險操作dangerous! dangerous! dangerous!
    list.forEach {
        println(it)
    }
}

理解:不變實際上就更好理解了,因為不存在子類型化關系,沒有所謂的子類型 A 的值在任何地方任何時候可以替換超類型 B 的值的規則,所以上述例子編譯不過,對于 printList 函數而言必須接收的類型是 MutableList<Any>,因為一旦傳入和它不一樣的具體類型就會存在危險操作,出現不安全的問題。

8. 總結

到這里有關 Kotlin 中泛型型變的核心概念就闡述完畢了,文章有點長可以需要好好消化,重在理解以及一些實際的場景上應用。其實對于 Kotlin 協變和不變倒是很好理解,可能大家對于逆變還是需要好好理解。下篇文章將是泛型型變的一些應用和一些其他概念研究比如星投影之類的。