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

極客熊貓發表於2018-11-08

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

  • 第一,深入理解泛型中每個小概念和結論,最好能用自己的話表述出來;
  • 第二,通過分析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)
    }
}
複製程式碼

上述操作是合法的,執行結果如下

教你如何攻克Kotlin中泛型型變的難點(上篇)
如果我們上述的函式形參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)
    }
}
複製程式碼

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

我們來試想下,利用反證法驗證下,假如上述程式碼編譯通過了,會發生什麼,就會發生下面的可能出現類似的危險操作. 就會出現一個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>型別。

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

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

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

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

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
    
    ...
  }
複製程式碼

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

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型別的超型別。可以明顯看出子型別的規則會比子類規則更為寬鬆。那麼我們可以一起分析下面幾個例子:

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

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

再來看個例子,所有類的非空型別都是該類對應的可空型別的子型別,但是反過來說就不行,就比如String非空型別是String?可空型別的子型別,很明顯嘛,任何String?可空型別出現值的地方,都可以使用String非空型別的值來替換。其實這些我在開發過程中是可以體會得到的,比如細心的同學就會發現,我們在Kotlin開發過程,如果一個函式接收的是一個可空型別的引數,呼叫的地方傳入一個非空型別的實參進去是合法的。但是如果一個函式接收的是非空型別引數,傳入一個可空型別的實參編譯器就會提示你,可能存在空指標問題,需要做非空判斷。 因為我們知道非空型別比可空型別更安全。來幅圖理解下:

教你如何攻克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、最後說下,下篇文章就是泛型中的高階概念了,其實不用害怕,只要把這篇文章概念理解清楚了後面會很簡單的。

Kotlin系列文章,歡迎檢視:

原創系列:

翻譯系列:

實戰系列:

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

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

相關文章