簡述: 前幾天咱們一塊兒爲Kotlin中的泛型型變作了一個很好的鋪墊,深刻分析下類型和類,子類型和子類之間的關係、什麼是子類型化關係以及型變存在的意義。那麼今天將會講點更刺激的東西,也就是Kotlin泛型型變中最爲難理解的地方,那就是Kotlin中的協變、逆變、不變。雖然很難理解,可是有了上篇文章基礎教你如何攻克Kotlin中泛型型變的難點(上篇)理解起來仍是相對比較輕鬆。若是你是初學者不建議直接看這篇文章,仍是建議把該系列的上篇理解下。安全
扯會皮,這幾天我一直在思考一個問題,由於官方給出的結論太過於正式化,並且估計好點的開發者只是記住官方的結論和它的使用規則,可是並無真正去了解爲何是這樣的,這樣設計的意義何在呢?app
廢話很少說,繼續上本篇文章的思惟導圖less
還記得上篇的子類型化關係嗎?協變實際上就是保留子類型化關係,首先,咱們須要去明確一下這裏所說的保留子類型化關係是針對誰而言的呢?ide
來看個例子,String
是String?
的子類型,咱們知道基礎類型List<out E>
是協變的,那麼List<String>
也就是List<String?>
的子類型的。很明顯這裏針對的角色就是List<String>
和List<String?>
,是它們保留了String
到String?
的子類型化關係。或者換句話說兩個具備相同的基礎類型的泛型協變類型,若是類型實參具備子類型化關係,那麼這個泛型類型具備一致方向的子類型化關係。那麼具備子類型化關係實際上子類型的值能在任什麼時候候任何地方替代超類型的值。函數
interface Producer<out T> {//在泛型類型形參前面指定out修飾符
val something: T
fun produce(): T
}
複製代碼
從上面定義的基本結構來看,實際上協變點就是上面produce
函數返回值的T
的位置,Kotlin中規定一個泛型協變類,在泛型形參前面加上out修飾後,那麼修飾這個泛型形參在函數內部使用範圍將受到限制只能做爲函數的返回值或者修飾只讀權限的屬性。源碼分析
interface Producer<out T> {//在泛型類型形參前面指定out修飾符
val something: T//T做爲只讀屬性的類型,這裏T的位置也是out協變點
fun produce(): T//T做爲函數的返回值輸出給外部,這裏T的位置就是out協變點
}
複製代碼
以上協變點都是標準的T類型,實際上如下這種方式其實也是協變點,請注意體會協變點含義:post
interface Producer<out T> {
val something: List<T>//即便T不是單個的類型,可是它做爲一個泛型類型修飾只讀屬性,因此它所處位置仍是out協變點
fun produce(): List<Map<String,T>>//即便T不是單個的類型,可是它做爲泛型類型的類型實參修飾返回值,因此它所處位置仍是out協變點
}
複製代碼
協變點基本特徵: 若是一個泛型類聲明成協變的,用out修飾的那個類型形參,在函數內部出現的位置只能在只讀屬性的類型或者函數的返回值類型。相對於外部而言協變是生產泛型參數的角色,生產者向外輸出out學習
List<out E>
的源碼分析咱們在上篇文章中就說過Kotlin中的List
並非Java中的List
,由於Kotlin中的List
是個只讀的List不具有修改集合中元素的操做方法。Java的List
實際上至關於Kotlin中的MutableList
具備各類讀和寫的操做方法。ui
Kotlin中的List<out E>
實際上就是協變的例子,用它來講明分析協變最好不過了,還記得上篇文章說過的學習泛型步驟二嗎,就是經過分析源碼來驗證本身的理解和結論。經過如下源碼都可驗證咱們上述所說的結論。this
//經過泛型類定義能夠看出使用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協變的位置是返回值的類型以及只讀屬性的類型(這點源碼中沒有表現出來,可是實際上倒是如此啊,這裏能夠自行查閱其餘例子)
逆變實際上就是和協變子類型化關係正好相反,它是反轉子類型化關係。
來個例子說明下,咱們知道String
是String?
的子類型,Comparable<in T>
是逆變的,那麼Comparable<String>
到Comparable<String?>
其實是反轉了String
到String?
的子類型化關係,也就是和String
到String?
的子類型化關係相反,那麼Comparable<String?>
就是Comparable<String>
子類型, Comparable<String>
類型值出現的地方均可用Comparable<String?>
類型值來替代。
換句話說就是:兩個具備相同的基礎類型的泛型逆變類型,若是類型實參具備子類型化關係,那麼這個泛型類型具備相反方向的子類型化關係
interface Consumer<in T>{//在泛型類型形參前面指定in修飾符
fun consume(value: T)
}
複製代碼
從上面定義的基本結構來看,實際上逆變點就是上面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所在位置依然是函數形參類型,因此仍處於逆變點
}
複製代碼
逆變點基本特徵: 若是一個泛型類聲明成逆變的,用in修飾泛型類的類型形參,在函數內部出現的位置只能是做爲可變屬性的類型或者函數的形參類型。相對於外部而言逆變是消費泛型參數的角色,消費者請求外部輸入in
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函數的形參類型,能夠看出它是屬於消費泛型參數的
}
複製代碼
對於不變就更簡單了,泛型型變中除去協變、逆變就是不變了。其實不變看起來就是咱們經常使用的普通泛型,它既沒有in關鍵字修飾,也沒有out關鍵字修飾。它就是普通的泛型,因此很明顯它沒有像協變、逆變那樣那麼多的條條框框,它很自由既可讀又可寫,既能夠做爲函數的返回值類型也能夠做爲函數形參類型,既能夠聲明成只讀屬性的類型又能夠聲明可變屬性。可是注意了:不變型就是沒有子類型化關係,因此它會有一個侷限性就是若是以它做爲函數形參類型,外部傳入只能是和它相同的類型,由於它根本就不存在子類型化關係說法,那也就是沒有任何類型值可以替換它,除了它本身自己的類型 例如MutableList<String>和MutableList<String?>
是徹底兩種不同的類型,儘管String
是String?
子類型,可是基礎泛型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
}
複製代碼
思考一:
協變泛型類的泛型形參類型T必定就只能out協變點位置嗎?能不能在in逆變點位置呢?
解惑一: 能夠在逆變點,可是必須在函數內部保證該泛型參數T不存在寫操做行爲,只能有讀操做
出現的場景: 聲明瞭協變的泛型類,可是有時候須要從外部傳入一個該類型形參的函數參數,那麼這個形參類型就處於in逆變點的位置了,可是函數內部可以保證不會對泛型參數存在寫操做的行爲。常見例子就是List<out E>
源碼,就是上面你們一臉懵逼的地方,就是那個爲何定義成協變的泛型T跑到了函數形參類型上去。 以下面部分代碼所示:
override fun contains(element: @UnsafeVariance E): Boolean//咦! 咦! 咦! 和說的不同啊,爲何還能出如今這個位置,還出來了個@UnsafeVariance 這個是什麼鬼? 如今回答你就是可能會出如今這,可是隻要保證函數不會寫操做便可
複製代碼
上述的List中的contains函數形參就是泛型形參E,它是協變的出如今逆變點,可是隻要保證函數內部不會對它有寫操做便可
思考二:
逆變泛型類的泛型形參類型T就必定只能在in逆變點位置嗎?能不能在out協變點位置呢?
解惑二: 同理,也能夠在協變點位置
思考三:
能在其餘的位置嗎? 好比構造函數
解惑三: 能夠在構造器函數中,由於這是個比較特殊的位置,既不在in位置也不在out位置
class ClassMates<out T: Student>(vararg students: T){//能夠看到雖然定義成了協變,可是這裏的T不是在out協變點的位置,這種聲明依然是合法的
...
}
複製代碼
注意: 這裏就是很特殊的場景了,因此開頭就說過了若是把這些規則,用法只是死記硬背下來,碰到這種場景的時候就開始懷疑人生了,規則中不是這樣的啊,規則中定義協變點就是隻讀屬性類型和函數返回值類型的位置啊,這個位置不上不下的該怎麼解釋呢?因此解決問題仍是須要抓住問題的關鍵纔是最主要的。
其實解釋這個問題也不難,回到型變的目的和初衷上去,型變是爲了解決類型安全問題,是防止更加泛化的實例調用某些存在危險操做的方法。構造函數很特殊通常建立後實例對象後,在該對象基礎上構造函數是不能再被調用的,因此這裏T放在這裏是安全的。
思考四
爲了安全,我是否是隻要把全部泛型類全都定義成協變或逆變或不變一種就能夠了呢?
解惑四: 不行,這樣不安全,按照實際場景需求出發,一味定義成協變或逆變實際上限制了該泛型類對該類型形參使用的可能性,由於out只能是做爲生產者,協變點位置有限制,而in只能是消費者逆變點的位置也有限制。那索性全都定義成不變型,那就在另外一層面喪失了靈活性,就是它失去了子類型化關係, 就是把它做爲函數參數類型,外部只能傳入和它相同的類型,不可能存在子類型化關係的保留和反轉了
由上面的思考明白了一點,使用協變、逆變的時候並非那麼死的按照協變點,逆變點規則來,能夠更加靈活點,關鍵是不能違背協變、逆變根本宗旨。協變宗旨就是定義的泛型類內部不能存在寫操做的行爲,對於逆變根本宗旨通常都是隻寫的。那Kotlin中List<out E>
的源碼來講都不是真正規則上說的那樣協變,泛型形參E並不都是在協變點out上,可是List<out E>
內部可以保證不會存在寫操做危險行爲因此這種定義也是合法。實際上真正開發過程,很難作到協變泛型類中的泛型類型形參都是在out協變點上,由於有時候需求須要確實須要從外部傳入一個該類型形參的一個函數形參。
因此最終的結論是: 協變點out和逆變點in的位置的規則是通常大致狀況下要遵照的,可是須要具體狀況具體分析,針對設計的泛型類具體狀況,適當地在不違背根本宗旨以及知足需求狀況下變下協變點和逆變點的位置規則
由上面的本質區別分析,嚴格按照協變點、逆變點規則來是不能徹底知足咱們真實開發需求場景的,因此有時候須要一道後門,那就要用特殊方式告訴它。那就是使用UnSafeVariance註解。因此UnSafeVariance註解做用很簡單: 經過@UnSafeVariance告訴編譯器該處安全性本身可以把控,讓它放你編譯經過便可,若是不加編譯器認爲這是不合法的。註解的意思就是不安全的型變,例如在協變泛型類中有個函數是以傳入一個該泛型形參的函數形參的,經過UnSafeVariance註解讓編譯器閉嘴,而後把它放置在逆變點其實是增長一層危險性,至關於把這層危險交給了開發者,只要開發者能保證內部不存在危險性操做確定就是安全的。
將從基本結構形式、有無子類型化關係(保留、反轉)、有無型變點(協變點out、逆變點in)、角色(生產者輸出、消費者輸入)、類型形參存在的位置(協變就是修飾只讀屬性和函數返回值類型;逆變就是修飾可變屬性和函數形參類型)、表現特徵(只讀、可寫、可讀可寫)等方面進行對比
協變 | 逆變 | 不變 | |
---|---|---|---|
基本結構 | Producer<out E> |
Consumer<in T> |
MutableList<T> |
子類型化關係 | 保留子類型化關係 | 反轉子類型化關係 | 無子類型化關係 |
有無型變點 | 協變點out | 逆變點in | 無型變點 |
類型形參存在的位置 | 修飾只讀屬性類型和函數返回值類型 | 修飾可變屬性類型和函數形參類型 | 均可以,沒有約束 |
角色 | 生產者輸出爲泛型形參類型 | 消費者輸入爲泛型形參類型 | 既是生產者也是消費者 |
表現特徵 | 內部操做只讀 | 內部操做只寫 | 內部操做可讀可寫 |
實際上就是要明確何時該使用協變、何時該使用逆變、何時該使用不變。 實際上經過上述分析對比的表格能夠得出結論: 首先,表格有不少個條件特徵,究竟是先哪一個開始斷定條件好呢?實際上這裏面仍是須要選擇一下的。
假設一: 就好比一開始就以有無使用子類型化關係爲條件作斷定,這樣作法是有點問題的,試想下在實際開發中,先是去定義泛型類內部一些方法和屬性的,這時候很難知道在外部使用狀況下存不存在利用子類型化關係,也就是存不存在用子類型的值替換超類型的值場景,因此在剛剛定義泛型類的時候很難明確的。故仍是先從泛型類定義的內部特徵着手會更加明確點。
假設二: 好比先根據泛型類內部定義一些方法和屬性,因爲剛開始定義並不能肯定是不是協變out仍是逆變in,因此上面的有無型變點不能做爲斷定條件,最開始還沒肯定的時候通常當作不變泛型類來定義。
,最直白能夠先看看型變點,而後根據型變點基本肯定泛型類內部表現特徵,
補充一點,若是最終肯定是協變的,但是在定義的時候經過步驟1獲得類型形參存在的位置處於函數形參位置,那麼這時候就能夠大膽藉助@UnSafeVariance註解告訴編譯器使得編譯經過,逆變同理。
來張圖理解下
是否還記得上一篇文章開頭的那個例子和那幅漫畫圖
例子代碼以下:
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>
,由於一旦傳入和它不同的具體類型就會存在危險操做,出現不安全的問題。
因爲篇幅緣由,因此星投影和協變、逆變實際例子的應用放到下一篇應用篇去了,可是到這裏Kotlin泛型型變重點和難點已經所有講完,後面一篇也就是實際開發中例子的運用。關於這篇文章仍是須要好好消化一下,最後再根據下一篇實際例子就能夠更加鞏固,下篇將會注重講開發中的例子實現,不會再扣概念了。下篇敬請關注~~~
原創系列:
翻譯系列:
實戰系列:
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~