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

Kotlin 初識泛型型變

相信有很多初學者對 Kotlin 中的泛型型變都是一知半解,泛型型變概念太多了,而且每個概念和后面都是相關的,只要前面有一個地方未理解后面的難點更是越來越看不懂。Kotlin 的泛型比 Java 中的泛型多了一些新的概念,比如子類型化關系、逆變、協變、星投影的。個人認為學好 Kotlin 的泛型主要有這么幾個步驟:

  • 深入理解泛型中每個小概念和結論,最好能用自己的話表述出來;
  • 通過分析 Kotlin 中的相關源碼驗證你的理解和結論;
  • 通過實際的例子鞏固你的理解。

1. 泛型為什么會存在型變?

首先,我們需要明確兩個名詞概念: 基礎類型和實參類型。例如對于 List<String>, List 就是基礎類型而這里的 String 就是實參類型。

然后,我們需要明確一下,這里的型變到底指的是什么?可以先大概描述一下,它反映的是一種特殊類型的對應關系規則。是不是很抽象?

那就先來看個例子,例如 List<String>和List<Any> 他們擁有相同的基礎類型,實參類型 StringAny 存在父子關系,那么是不是 List<String>List<Any> 是否存在某種對應關系呢?實際上,我們討論的型變也就是圍繞著這種場景展開的。有了上面的認識,進入正題為什么需要這種型變關系呢?來看對比的例子,我們需要向一個函數中傳遞參數。

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<Any>,函數內部是不知道外部傳入是List<Int>還是List<String>,全部當做List<Any>處理
    list.forEach {
        println(it)
    }
}

上述操作是合法的,運行結果如下:
圖片描述
如果我們上述的函數形參 List<Any> 換成 MutableList<Any> 會變成什么樣呢?

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)
    }
}

圖片描述

我們來試想下,利用反證法驗證下,假如上述代碼編譯通過了,會發生什么,就會發生下面的可能出現類似的危險操作。就會出現一個 Int 或者 String 的集合中引入其他的非法數據類型,所以肯定是有問題的,故編譯不通過。

因為我們說過在函數的形參類型 MutableList<Any> 在函數內部它只知道是該類型也不知道外部給它傳了個啥,所以它只能在內部按照這個類型規則來,所以在函數內部 list.add(3.0f) 這行代碼時編譯通過的,向一個 MutableList<Any> 集合加入一個 Float 類型明顯說得過去的。

通過對比上面兩個例子,大家有沒有思考一個問題就是為什么 List<String>、List<Int>替換List<Any> 可以,而 MutableList<String>、MutableList<Int>替換MutableList<Any> 不可以呢?

實際上問題所說的類型替換其實就是型變 , 那大家到這就明白了為什么會存在型變了,型變更為了泛型接口更加安全,假如沒有型變,就會出現上述危險問題。

那另一問題來了為什么有的型變關系可以,有的不可以呢?對于傳入集合內部不會存在修改添加其元素的操作 (只讀),是可以支持外部傳入更加具體類型實參是安全的,而對于集合內部存在修改元素的操作 (寫操作) 是不安全的,所以編譯器不允許。

以上面例子分析,List<Any> 實際上一個只讀集合 (注意:它和 Java 中的 List 完全不是一個東西,注意區分),它內部不存在 add,remove 操作方法,不信的可以看下它的源碼,所以以它為形參的函數就可以敞開大門大膽接收外部參數,因為不存在修改元素操作所以是安全的,所以第一個例子是編譯 OK 的;

而對于 MutableList<Any> 在 Kotlin 中它是一個可讀可寫的集合,相當于 Java 中的 List, 所以它的內部存在著修改、刪除、添加元素的危險操作方法,所以對于外部傳入的函數形參它需要做嚴格檢查必須是 MutableList<Any> 類型。

為了幫助理解和記憶,自己繪制了一張獨具風趣的漫畫圖幫助理解,這張圖很重要以致于后面的協變、逆變、不變都可以從它獲得理解。后面也會不斷把它拿出來分析
圖片描述

最后為了徹底把這個問題分析透徹可以給大家看下 List<E>MutableList<E> 的部分源碼

圖片描述

public interface List<out E> : Collection<E> {
    // Query Operations
    override val size: Int

    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    
    ...
  }

圖片描述

public interface MutableList<E> : List<E>, MutableCollection<E> {
    // Modification Operations
    override fun add(element: E): Boolean

    override fun remove(element: E): Boolean

    // Bulk Modification Operations
    override fun addAll(elements: Collection<E>): Boolean
    ...
 }

仔細對比下 List<out E>MutableList<E> 泛型定義是不一樣的,他們分別對應了協變不變 , 至于什么是協變什么是逆變什么不變,我們后面會詳細講。

2. 子類、子類型、超類型概念梳理

我們一般說子類就是派生類,該類一般會繼承它的超類。例如: class Student: Person(), 這里的 Student 一般稱為 Person 的子類,PersonStudent 的超類。

子類型和超類型定義則完全不一樣,我們從上面類和類型區別就知道一個類可以有很多類型,那么子類型不僅僅是想子類那樣繼承關系那么嚴格。

子類型定義的規則一般是這樣的: 任何時候如果需要的是 A 類型值的任何地方,都可以使用 B 類型的值來替換的,那么就可以說 B 類型是 A 類型的子類型或者稱 A 類型是 B 類型的超類型??梢悦黠@看出子類型的規則會比子類規則更為寬松。那么我們可以一起分析下面幾個例子:

圖片描述

Tips:某個類型也是它自己本身的子類型,很明顯 Person 類型的值任意出現地方,Person 肯定都是可以替換的。屬于子類關系的一般也是子類型關系。像 String 類型值肯定不能替代 Int 類型值出現的地方,所以它們不存在子類型關系

再來看個例子,所有類的非空類型都是該類對應的可空類型的子類型,但是反過來說就不行,就比如 Person 非空類型是 Person? 可空類型的子類型,很明顯嘛,任何 Person? 可空類型出現值的地方,都可以使用 Person 非空類型的值來替換。

其實這些我在開發過程中是可以體會得到的,比如細心的同學就會發現,我們在 Kotlin 開發過程,** 如果一個函數接收的是一個可空類型的參數,調用的地方傳入一個非空類型的實參進去是合法的。** 但是如果一個函數接收的是非空類型參數,傳入一個可空類型的實參編譯器就會提示你,可能存在空指針問題,需要做非空判斷。因為我們知道非空類型比可空類型更安全。來幅圖理解下:

圖片描述

3. 什么是子類型化關系

相信到了這,大家應該自己都能猜出什么是子類型化關系吧?它是實際上就是我們上面所講的那些。

3.1 子類型化關系

大致概括一下: 如果 A 類型的值在任何時候任何地方出現都能被 B 類型的值替換,B 類型就是 A 類型的子類型,那么 B 類型到 A 類型之間這種映射替換關系就是子類型化關系

3.2 回答最開始的問題

現在我們也能用 Kotlin 中較為專業的術語子類型化關系來解釋最開始那個問題為什么以 List<String>,List<Int> 類型的函數實參可以傳遞給 List<Any> 類型的函數形參,而 MutableList<String>,MutableList<Int> 類型的函數實參不可以傳遞給 MutableList<Any> 類型的函數形參?

因為 List<String>,List<Int> 類型是 List<Any> 類型的子類型,所以 List<Any> 類型值出現的地方都可以使用 List<String>,List<Int> 類型的值來替換。而 MutableList<String>,MutableList<Int> 類型不是 MutableList<Any> 的子類型也不是它的超類型,所以當然就不能替換了。

3.2 由上面回答引出一個細節點

仔細分析觀察下上面所說的,List<String>,List<Int> 類型是 List<Any> 類型的子類型,然后再細看針對都具有相同的 List 這個基礎類型的泛型參數類型對應關系,這里的 String,Int 類型是 Any 類型的子類型 (注意:我們在泛型中都應該站在類型和子類型的角度來看問題,不要在局限于類和子類繼承層面啊,這點很重要,因為 List<String> 還是 List<String?> 子類型呢,所以和繼承層面子類沒有關系),然后 List<String>,List<Int> 類型也是 List<Any> 類型的子類型,這種關系叫做保留子類型化關系,也就是所謂的協變。具體下篇文章著重分析。

4. 總結

本篇文章可以說是下篇文章的一個概念理解的基礎,下篇很多高級的概念和原理都是在這篇文章延伸的,建議好好消化這些概念,這里最后再著重強調一下:一定需要好好理解什么是子類型,它和子類有什么區別。實際上 Kotlin 中的泛型型變的基礎就是子類型化關系,一般在這我們都是站在類型和子類型角度分析關系,而不是簡單的類和子類繼承層面。還有就是有沒有思考過為什么要弄這么一套復雜的型變關系,其實仔細想想就為了泛型類操作和使用更加安全,避免引入一些存在危險隱患。下篇文章就是泛型中的高級概念,其中包括泛型協變、逆變等,只要把這篇文章概念理解清楚了后面會很簡單的。