Kotlin系列文章,歡迎查看:

原創系列: Kotlin的獨門秘籍Reified實化類型參數(下篇) 有關Kotlin屬性代理你需要知道的一切 淺談Kotlin中的Sequences源碼解析 淺談Kotlin中集合和函數式API完全解析-上篇 淺談Kotlin語法篇之lambda編譯成位元組碼過程完全解析 淺談Kotlin語法篇之Lambda表達式完全解析 淺談Kotlin語法篇之擴展函數 淺談Kotlin語法篇之頂層函數、中綴調用、解構聲明 淺談Kotlin語法篇之如何讓函數更好地調用 淺談Kotlin語法篇之變數和常量 * 淺談Kotlin語法篇之基礎語法

翻譯系列: [譯]Kotlin的獨門秘籍Reified實化類型參數(上篇) [譯]Kotlin泛型中何時該用類型形參約束? [譯] 一個簡單方式教你記住Kotlin的形參和實參 [譯]Kotlin中是應該定義函數還是定義屬性? [譯]如何在你的Kotlin代碼中移除所有的!!(非空斷言) [譯]掌握Kotlin中的標準庫函數: run、with、let、also和apply [譯]有關Kotlin類型別名(typealias)你需要知道的一切 [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)? [譯]Kotlin中的龜(List)兔(Sequence)賽跑 [譯]Effective Kotlin系列之考慮使用靜態工廠方法替代構造器 * [譯]Effective Kotlin系列之遇到多個構造器參數要考慮使用構建器 實戰系列: 用Kotlin擼一個圖片壓縮插件ImageSlimming-導學篇(一) 用Kotlin擼一個圖片壓縮插件-插件基礎篇(二) 用Kotlin擼一個圖片壓縮插件-實戰篇(三) 淺談Kotlin實戰篇之自定義View圖片圓角簡單應用

簡述: Kotlin中泛型相關的文章也幾乎接近尾聲,但到後面也是泛型的難點和重點。相信有很多初學者對Kotlin中的泛型型變都是一知半解,比如我剛開始接觸就是一臉懵逼,概念太多了,而且每個概念和後面都是相關的,只要前面有一個地方未理解後面的難點更是越來越看不懂。Kotlin的泛型比Java中的泛型多了一些新的概念,比如子類型化關係、逆變、協變、星投影的。個人認為學好Kotlin的泛型主要有這麼幾個步驟: 第一,深入理解泛型中每個小概念和結論,最好能用自己的話表述出來; 第二,通過分析Kotlin中的相關源碼驗證你的理解和結論; * 第三,就是通過實際的例子鞏固你的理解;

由於泛型型變涉及的內容比較多,所以將它分為上下兩篇,廢話不多說請看以下導圖:

一、為什麼會存在型變?

首先,我們需要明確兩個名詞概念: 基礎類型和實參類型。例如對於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>泛型定義是不一樣的,他們分別對應了協變不變,至於什麼是協變什麼是逆變什麼不變,我們後面會詳細講。

二、類、類型、子類、子類型、超類型概念梳理

看到標題可能大家會有點納悶, 類和類型不是一個東西嗎?我平時都是把它們當做一個東西來用的啊。實際上是不一樣的,在這裡我們需要去一一扣概念去理解,以便後面更好理解型變關係。那麼我們一起看下它們到底有哪些不一樣的?

我們可以把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開發過程,如果一個函數接收的是一個可空類型的參數,調用的地方傳入一個非空類型的實參進去是合法的。但是如果一個函數接收的是非空類型參數,傳入一個可空類型的實參編譯器就會提示你,可能存在空指針問題,需要做非空判斷。 因為我們知道非空類型比可空類型更安全。來幅圖理解下:

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

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

子類型化關係:

大致概括一下: 如果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>類型的子類型,這種關係叫做保留子類型化關係,也就是所謂的協變。具體我會下篇著重分析。

四、結語

本篇文章可以說是下篇文章的一個概念理解的基礎,下篇很多高級的概念和原理都是在這篇文章延伸的,建議好好消化這些概念,這裡最後再著重強調幾點:

  • 1、一定需要好好理解什麼是子類型,它和子類有什麼區別。實際上Kotlin中的泛型型變的基礎就是子類型化關係啊,一般在這我們都是站在類型和子類型角度分析關係,而不是簡單的類和子類繼承層面啊。
  • 2、還有就是大家有沒有思考過為什麼要弄這麼一套型變關係啊,其實仔細想想就為了泛型類操作和使用更加安全,避免引入一些存在危險隱患,造成泛型不安全,具體可以看看本文前面畫的一張醜陋的漫畫。所以也不得不佩服設計出這套規則語言開發者思想所折服啊。
  • 3、最後說下,下篇文章就是泛型中的高級概念了,其實不用害怕,只要把這篇文章概念理解清楚了後面會很簡單的。

weixin.qq.com/r/aUjI0Lv (二維碼自動識別)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每周會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

推薦閱讀:

相关文章