教你如何攻克Kotlin中泛型型變的難點(上篇)

簡述: Kotlin中泛型相關的文章也幾乎接近尾聲,但到後面也是泛型的難點和重點。相信有不少初學者對Kotlin中的泛型型變都是隻知其一;不知其二,好比我剛開始接觸就是一臉懵逼,概念太多了,並且每一個概念和後面都是相關的,只要前面有一個地方未理解後面的難點更是愈來愈看不懂。Kotlin的泛型比Java中的泛型多了一些新的概念,好比子類型化關係、逆變、協變、星投影的。我的認爲學好Kotlin的泛型主要有這麼幾個步驟:安全

  • 第一,深刻理解泛型中每一個小概念和結論,最好能用本身的話表述出來;
  • 第二,經過分析Kotlin中的相關源碼驗證你的理解和結論;
  • 第三,就是經過實際的例子鞏固你的理解;

因爲泛型型變涉及的內容比較多,因此將它分爲上下兩篇,廢話很少說請看如下導圖:app

1、爲何會存在型變?

首先,咱們須要明確兩個名詞概念: 基礎類型和實參類型。例如對於List<String>, List就是基礎類型而這裏的String就是實參類型ide

而後,咱們須要明確一下,這裏的型變到底指的是什麼?函數

能夠先大概描述一下,它反映的是一種特殊類型的對應關係規則。是否是很抽象?那就先來看個例子,例如List<String>和List<Any>他們擁有相同的基礎類型,實參類型StringAny存在父子關係,那麼是否是List<String>List<Any>是否存在某種對應關係呢? 實際上,咱們討論的型變也就是圍繞着這種場景展開的。post

有了上面的認識,進入正題爲何須要這種型變關係呢?來看對比的例子,咱們須要向一個函數中傳遞參數。ui

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)
    }
}
複製代碼

上述操做是合法的,運行結果以下spa

若是咱們上述的函數形參 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、類、類型、子類、子類型、超類型概念梳理

看到標題可能你們會有點納悶, 類和類型不是一個東西嗎?我平時都是把它們當作一個東西來用的啊。其實是不同的,在這裏咱們須要去一一扣概念去理解,以便後面更好理解型變關係。那麼咱們一塊兒看下它們到底有哪些不同的?

咱們能夠把Kotlin中的類可分爲兩大類: 泛型類非泛型類

  • 非泛型類

先說非泛型類也就是開發中接觸最多的通常類,通常的類去定義一個變量的時候,它的實際就是這個變量的類型。例如: var msg: String 這裏咱們能夠說Stringmsg變量的類型是一致的。可是在Kotlin中還有一種特殊的類型那就是可空類型,能夠定義爲var msg: String?,這裏的Stringmsg變量的String?類型就不同了。因此在Kotlin中一個通常至少對應兩種類型. 因此類和類型不是一個東西。

  • 泛型類

泛型類比非泛型類要更加複雜,實際上一個泛型類能夠對應無限種類型。爲何這麼說,其實很容易理解。咱們從前面文章知道,在定義泛型類的時候會定義泛型形參,要想拿到一個合法的泛型類型就須要在外部使用地方傳入具體的類型實參替換定義中的類型形參。咱們知道在Kotlin中List是一個類,它不是一個類型。由它能夠衍生成無限種泛型類型例如List<String>、List<Int>、List<List<String>>、List<Map<String,Int>>

  • 子類、子類型和超類型

咱們通常說子類就是派生類,該類通常會繼承它的父類(也叫基類)。例如: class Student: Person(),這裏的Student通常稱爲Person的子類

子類型則不同,咱們從上面類和類型區別就知道一個類能夠有不少類型,那麼子類型不只僅是想子類那樣繼承關係那麼嚴格。 子類型定義的規則通常是這樣的: 任什麼時候候若是須要的是A類型值的任何地方,均可以使用B類型的值來替換的,那麼就能夠說B類型是A類型的子類型或者稱A類型是B類型的超類型。能夠明顯看出子類型的規則會比子類規則更爲寬鬆。那麼咱們能夠一塊兒分析下面幾個例子:

注意: 某個類型也是它本身自己的子類型,很明顯String類型的值任意出現地方,String確定都是能夠替換的。屬於子類關係的通常也是子類型關係。像String類型值確定不能替代Int類型值出現的地方,因此它們不存在子類型關係

再來看個例子,全部類的非空類型都是該類對應的可空類型的子類型,可是反過來講就不行,就好比String非空類型是String?可空類型的子類型,很明顯嘛,任何String?可空類型出現值的地方,均可以使用String非空類型的值來替換。其實這些我在開發過程當中是能夠體會獲得的,好比細心的同窗就會發現,咱們在Kotlin開發過程,若是一個函數接收的是一個可空類型的參數,調用的地方傳入一個非空類型的實參進去是合法的。可是若是一個函數接收的是非空類型參數,傳入一個可空類型的實參編譯器就會提示你,可能存在空指針問題,須要作非空判斷。 由於咱們知道非空類型比可空類型更安全。來幅圖理解下:

3、什麼是子類型化關係?

我相信到了這,你們應該本身都能猜出什麼是子類型化關係吧?它是實際上就是咱們上面所講的那些。

子類型化關係:

大體歸納一下: 若是A類型的值在任什麼時候候任何地方出現都能被B類型的值替換,B類型就是A類型的子類型,那麼B類型到A類型之間這種映射替換關係就是子類型化關係

回答最開始的問題

如今咱們也能用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>的子類型也不是它的超類型,因此固然就不能替換了。

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

仔細分析觀察下上面所說的,List<String>,List<Int>類型是List<Any>類型的子類型,而後再細看針對都具備相同的List這個基礎類型的泛型參數類型對應關係, 這裏的String,Int類型是Any類型的子類型(注意: 咱們在泛型中都應該站在類型和子類型的角度來看問題,不要在侷限於類和子類繼承層面啊,這點很重要,由於List<String>仍是List<String?>子類型呢,因此和繼承層面子類沒有關係),而後List<String>,List<Int>類型也是List<Any>類型的子類型,這種關係叫作保留子類型化關係,也就是所謂的協變。具體我會下篇着重分析。

4、結語

本篇文章能夠說是下篇文章的一個概念理解的基礎,下篇不少高級的概念和原理都是在這篇文章延伸的,建議好好消化這些概念,這裏最後再着重強調幾點:

  • 一、必定須要好好理解什麼是子類型,它和子類有什麼區別。實際上Kotlin中的泛型型變的基礎就是子類型化關係啊,通常在這咱們都是站在類型和子類型角度分析關係,而不是簡單的類和子類繼承層面啊。

  • 二、還有就是你們有沒有思考過爲何要弄這麼一套型變關係啊,其實仔細想一想就爲了泛型類操做和使用更加安全,避免引入一些存在危險隱患,形成泛型不安全,具體能夠看看本文前面畫的一張醜陋的漫畫。因此也不得不佩服設計出這套規則語言開發者思想所折服啊。

  • 三、最後說下,下篇文章就是泛型中的高級概念了,其實不用懼怕,只要把這篇文章概念理解清楚了後面會很簡單的。

Kotlin系列文章,歡迎查看:

原創系列:

翻譯系列:

實戰系列:

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索