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

簡述: 這是泛型型變最後一篇文章了,也是泛型介紹的最後一篇文章。順便再扯點別的,上週去北京參加了JetBrains 2018開發者日,主要是參加Kotlin專場。我的感受收穫仍是挺多的,bennyHuo和彥偉老師精彩演講確實傳遞不少乾貨啊,固然還有Hali佈道師大佬帶來了的Kotlin1.3版本的新特性以及Google中國技術推廣負責人鍾輝老師帶來的Coroutines在Android開發中的應用。因此準備整理以下幾篇文章爲後續發佈:java

  • 一、Kotlin中1.3版本新特性都有哪些?
  • 二、Kotlin中的Coroutine(協程)在Android上應用(協程學前班篇)
  • 三、Ktor異步框架初體驗(Ktor學前班篇)
  • 四、Kotlin中data class的使用(benny大佬在大會上講的很清楚了,也很全面。主要講下我的以前踩過的坑,特別是用於後端開發坑更多)

那麼今天這篇文章主要是爲了給上篇型變文章兩個尾巴以及泛型型變是如何被應用到實際開發中的去。而且我會用上篇博客如何去選擇相應型變的方法一步步肯定最終咱們該使用協變、逆變、仍是不變,我會用一個實際例子來講明。這篇文章比較簡單主要就如下四點:後端

  • 一、Kotlin聲明點變型與Java中的使用點變型進行對比
  • 二、如何使用Kotlin中的使用點變型
  • 三、Kotlin泛型中的星投影
  • 四、使用泛型型變實現可用於實際開發中的Boolean擴展

1、Kotlin聲明點變型與Java中的使用點變型進行對比

一、聲明點變型和使用點變型定義區別

首先,解釋下什麼是聲明點變型和使用點變型,聲明點變型顧名思義就是在定義聲明泛型類的時候指明型變類型(協變、逆變、不變),在Kotlin上表現形式就是在聲明泛型類時候在泛型形參前面加in或out修飾。使用點變型就是在每次使用該泛型類的時候都要去明確指出型變關係,若是你對Java中型變熟悉的話,Java就是使用了使用點變型.安全

二、二者優勢對比

聲明點變型:app

  • 有個明顯優勢就是隻須要在泛型類聲明時定義一次型變對應關係就能夠了,那麼以後無論在任何地方使用它都不用顯示指定型變對應關係,而使用點變型就是每處使用的地方都得重複定義一遍特別麻煩(又找到一處Kotlin優於Java的地方)。

使用點變型:框架

  • 實際上使用點變型也是有使用場景的,可使用的更加靈活;因此Kotlin並無徹底摒棄這個語法點,下面會專門介紹它的使用場景。

三、使用對比

剛剛說使用點變型特別麻煩,一塊兒來看看到底有多麻煩。這裏就是以Java爲表明,咱們都知道Java中要使用型變,是利用?通配符加(super/extends)來達到目的,例如: Function<? super T, ? extends E>, 其中的? extends E就是對應了協變,而? super T對應的是逆變。這裏以Stream API中的flatMap函數源碼爲例異步

@FunctionalInterface
public interface Function<T, R> {//聲明處就不用指定型變關係
    ...
}

//能夠看到使用點變型很是麻煩,定義一個mapper的Function泛型類參數時,還須要指明後面一大串Function<? super T, ? extends Stream<? extends R>>
  <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
複製代碼

聲明點變型到底有多方便,這裏就以Kotlin爲例,Kotlin使用in, out來實現型變對應規則。這裏以Sequences API中的flapMap函數源碼爲例ide

public interface Sequence<out T> {//Sequence定義處聲明瞭out協變
    /** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */
    public operator fun iterator(): Iterator<T>
}

public fun <T, R> Sequence<T>.flatMap(transform: (T) -> Sequence<R>): Sequence<R> {//能夠看到因爲Sequence聲明瞭協變,因此flatMap函數Sequence中的泛型實參R就不用再次指明型變類型了
    return FlatteningSequence(this, transform, { it.iterator() })
}
複製代碼

經過以上源碼對比,明顯看出Kotlin中的聲明點變型要比Java中的使用點變型要簡單得多吧。可是呢使用點變型並非一無可取,它在Kotlin中仍是有必定的使用場景的。下面即將揭曉函數

2、如何使用Kotlin中的使用點變型

實際上使用點變型在Kotlin中仍是有必定的使用場景,想象一下這樣一個實際場景,儘管某個泛型類是不變的,也就是具備可讀可寫的操做,但是有時候在某個函數中,咱們通常僅僅只用到只讀或只寫操做,這時候利用使用點變型它能使一個不變型的縮小型變範圍蛻化成協變或逆變的。是否是忽然懵逼了,用源碼來講話,你就明白了,一塊兒來看個源碼中的例子。post

Kotlin中的MutableCollection<E>是不變的,一塊兒來看了下它的定義ui

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {//沒有in和out修飾,說明是不變
    override fun iterator(): MutableIterator<E>
    public fun add(element: E): Boolean
    public fun remove(element: E): Boolean
    public fun addAll(elements: Collection<E>): Boolean
    public fun removeAll(elements: Collection<E>): Boolean
    public fun retainAll(elements: Collection<E>): Boolean
    public fun clear(): Unit
}
複製代碼

而後咱們接着看filter和filterTo函數的源碼定義

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

//注意: 這裏<T, C : MutableCollection<in T>>, MutableCollection<in T>聲明成逆變的了,是否是很奇怪啊,以前明明有說它是不變的啊,怎麼這裏就聲明逆變了
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
複製代碼

經過上面的函數是否是發現和MutableCollection不變相違背啊,實際上不是的。這裏就是一種典型的使用點變型的使用,咱們能夠再仔細分析下這個函數,destination在filterTo函數的內部只作了寫操做,遍歷Iterable中的元素,並把他們add操做到destination集合中,能夠驗證咱們上述的結論了,雖然MutableCollection是不變的,可是在函數內部只涉及到寫操做,徹底就可使用 使用點變型將它指定成一個逆變的型變類型,由不變退化成逆變明顯不會影響泛型安全因此這裏處理是徹底合法的。能夠再去看其餘集合操做API,不少地方都使用了這種方式。

上述關於不變退化到逆變的,這裏再講個不變退化到協變的例子。

//能夠看到source集合泛型類型聲明成了out協變了???
fun <T> copyList(source: MutableList<out T>, destination: MutableList<T>): MutableList<T>{
    for (element in source) destination.add(element)
}
複製代碼

MutableList<E>就是前面常說的不變的類型,一樣具備可讀可寫操做,可是這裏的source的集合泛型類型聲明成了out協變,會不會又蒙了。應該不會啊,有了以前逆變的例子,應該你們都猜到爲何了。很簡單就是由於在copyList函數中,source集合沒有涉及寫操做只有讀操做,因此可使用 使用點變型將MutableList的不變型退化成協變型,並且很顯然不會引入泛型安全的問題。

因此通過上述例子和之前例子關於如何使用逆變、協變、不變。仍是我以前說那句話,不要去死記規則,關鍵在於使用場景中讀寫操做是否引入泛型類型安全的問題。若是明確讀寫操做的場景了徹底能夠按照上述例子那樣靈活運用泛型的型變的,能夠程序寫得更加完美。

3、Kotlin泛型中的星投影

一、星投影的定義

星投影是一種特殊的星號投影,它通常用來表示不知道關於泛型實參的任何信息,換句話說就是它表示一種特定的類型,可是隻是這個類型不知道或者不能被肯定而已。

二、MutableList<*>MutableList<Any?>區別

首先咱們須要注意和明確的一點就是MutableList<*>MutableList<Any?>是不同的,MutableList<*>表示包含某種特定類型的集合;而MutableList<Any?>則是包含任意類型的集合。特定類型集合只不過不太肯定是哪一種類型,任意類型表示包含了多種類型,區別在於特定集合類型一旦肯定類型,該集合只能包含一種類型;而任意類型就能夠包含多種類型了。

三、MutableList<*>實際上一個out協變投影

MutableList<*>其實是投影成MutableList<out Any?>類型

首先,咱們來分析下爲何會這樣投影,咱們知道MutableList<*>只包含某種特定類型的集合,多是String、Int或者其餘類型中的一種,可想而知對於該集合操做須要禁止寫操做,不能往該集合中寫入數據,由於沒法肯定該集合的特定類型,寫操做極可能引入一個不匹配類型到集合中,這是一件很危險的事。可是反過來想下,若是該集合存在只讀操做,讀出數據元素類型雖然不知道,可是始終是安全的。只存在讀操做那麼說明是協變,協變就會存在保留子類型化關係,也就是讀出數據元素類型是不肯定類型子類型,那麼可想而知它只替換Any?類型的超類型,由於Any?是全部類型的超類型,那麼保留型化關係,因此MutableList<*>實際上就是MutableList<out Any?>的子類型了。

4、使用泛型型變實現可用於實際開發中的Boolean擴展

關於Boolean擴展的實現,主要來源於看了BennyHuo大佬寫的一些代碼中發現的,原來能夠這麼方便的寫if-else,因而乎就去看了下它的實現 可能不少人都知道了它的實現,爲何要講這個由於這是Kotlin泛型協變實際應用一個很是不錯的例子。

一、爲何開發一個Boolean擴展

給出一個例子場景,判斷一堆數集合中是否全是奇數,若是全是返回輸出"奇數集合",若是不是請輸出"不是奇數集合"

首先問下你們是否寫過一下相似下面代碼

//java版寫法

public void isOddList(){
    int count = 0;
    for(int i = 0; i < numberList.size(); i++){
        if(numberList[i] % 2 == 1){
            count++;
        }
    }
    if(count == numberList.size()){
       System.out.println("奇數集合");
       return;
    }
    System.out.println("不是奇數集合");
}

複製代碼
//kotlin版寫法

fun isOddList() = println(if(numberList.filter{ it % 2 == 1}.count().equals(numberList.size)){"奇數集合"} else {"不是奇數集合"})
複製代碼
//Boolean擴展版本寫法
fun isOddList() = println(numberList
          .filter{ it % 2 == 1 }
          .count()
          .equals(numberList.size)
          .yes{"奇數集合"}
          .otherwise{"不是奇數集合"})//有沒有發現Boolean擴展這種鏈式調用更加絲滑
複製代碼

對比發現,雖然Kotlin中的if-else表達式自帶返回值的,可是if-else的結構會打斷鏈式調用,可是若是使用Boolean擴展,徹底可使你的鏈式調用更加絲滑順暢一路調用到底。

二、Boolean擴展使用場景

Boolean擴展的使用場景我的認爲有兩個:

  • 配合函數式API一塊兒使用,遇到if-else判斷的時候建議使用Boolean擴展,由於它不會像if-else結構同樣會打斷鏈式調用的結構。
  • 另外一場景就是if的判斷條件組合不少,若是在外層再包裹一個if代碼顯得更加臃腫了,此時使用Boolean會使代碼更簡潔。

三、Boolean代碼實現

經過觀察上述Boolean擴展的使用,咱們首先須要明確幾點:

  • 第一點:咱們知道yes、otherwise實際上就是兩個函數,爲何能鏈式連接起來講明中間確定有一個相似橋樑做用的中間類型做爲函數的返回值類型。
  • 第二點:yes、otherwise函數的做用域是帶返回值的,例如上述例子它能直接返回字符串類型的數據。
  • 第三點: yes、oterwise函數的都是一個lamba表達式,而且這個lambda表達式將最後表達式中的值返回
  • 第四點: yes函數是在Boolean類型調用,因此須要基於Boolean類型的實現擴展函數

那麼根據以上得出幾點特徵基本能夠把這個擴展的簡單版本寫出來了(暫時不支持帶返回值的)

//做爲中間類型,實現鏈式連接
sealed class BooleanExt 
object Otherwise : BooleanExt()
object TransferData : BooleanExt()

fun Boolean.yes(block: () -> Unit): BooleanExt = when {
    this -> {
        block.invoke()
        TransferData//因爲返回值是BooleanExt,因此此處也須要返回一個BooleanExt對象或其子類對象,故暫且定義TransferData object繼承BooleanExt
    }
    else -> {//此處爲else,那麼須要連接起來,因此須要返回一個BooleanExt對象或其子類對象,故定義Otherwise object繼承BooleanExt
        Otherwise
    }
}

//爲了連接起otherwise方法操做因此須要寫一個BooleanExt類的擴展
fun BooleanExt.otherwise(block: () -> Unit) = when (this) {
    is Otherwise -> block.invoke()//判斷此時子類,若是是Otherwise子類執行block
    else -> Unit//不是,則直接返回一個Unit便可
}


fun main(args: Array<String>) {
    val numberList: List<Int> = listOf(1, 2, 3)
    //使用定義好的擴展
    (numberList.size == 3).yes {
        println("true")
    }.otherwise {
        println("false")
    }
}
複製代碼

上述的簡單版基本上把擴展的架子搭出來可是呢,惟一沒有實現返回值的功能,加上返回值的功能,這個最終版本的Boolean擴展就實現了。

如今來改造一下原來的版本,要實現返回值那麼block函數不能再返回Unit類型,應該要返回一個泛型類型,還有就是TransferData不能使用object對象表達式類型,由於須要利用構造器傳入泛型類型的參數,因此TransferData用普通類替代就行了。

關因而定義成協變、逆變仍是不變型,咱們能夠借鑑上篇文章使用到流程選擇圖和對比表格

將從基本結構形式、有無子類型化關係(保留、反轉)、有無型變點(協變點out、逆變點in)、角色(生產者輸出、消費者輸入)、類型形參存在的位置(協變就是修飾只讀屬性和函數返回值類型;逆變就是修飾可變屬性和函數形參類型)、表現特徵(只讀、可寫、可讀可寫)等方面進行對比

協變 逆變 不變
基本結構 Producer<out E> Consumer<in T> MutableList<T>
子類型化關係 保留子類型化關係 反轉子類型化關係 無子類型化關係
有無型變點 協變點out 逆變點in 無型變點
類型形參存在的位置 修飾只讀屬性類型和函數返回值類型 修飾可變屬性類型和函數形參類型 均可以,沒有約束
角色 生產者輸出爲泛型形參類型 消費者輸入爲泛型形參類型 既是生產者也是消費者
表現特徵 內部操做只讀 內部操做只寫 內部操做可讀可寫

  • 第一步:首先根據類型形參存在位置以及表現特徵肯定
sealed class BooleanExt<T>

object Otherwise : BooleanExt<Any?>()

class TransferData<T>(val data: T) : BooleanExt<T>()//val修飾data

inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {//T處於函數返回值位置
    this -> {
        TransferData(block.invoke())
    }
    else -> Otherwise//注意: 此處是編譯不經過的
}

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {//T處於函數返回值位置
    is Otherwise ->
        block()
    is TransferData ->
        this.data
}
複製代碼

經過以上代碼咱們能夠基本肯定是協變或者不變,

  • 第二步:判斷是否存在子類型化關係

因爲yes函數else分支返回的是Otherwise編譯不經過,很明顯此處不是不變的,由於上述代碼就是按照不變方式來寫的。因此基本肯定就是協變。

而後接着改,首先將sealed class BooleanExt<T>改成sealed class BooleanExt<out T>協變聲明,而後發現Otherwise仍是報錯,爲何報錯啊,報錯緣由是由於yes函數要求返回一個BooleanExt<T>類型,而此時返回Otherwise是個BooleanExt<Any?>(),反證法,假如上述是合理,那麼也就是BooleanExt<Any?>要替代BooleanExt<T>出現的地方,BooleanExt<Any?>BooleanExt<T>子類型,因爲BooleanExt<T>協變的,保留子類型型化關係也就是Any?T子類型,明顯不對吧,咱們都知道Any?是全部類型的超類型。因此原假設明顯不成立,因此編譯錯誤很正常,那麼逆向思考下,我是否是隻要把Any?位置用全部的類型的子類型Nothing來替換不就符合了嗎,那麼咱們天然而然就想到Nothing,在Kotlin中Nothing是全部類型的子類型。因此最終版本Boolean擴展代碼以下

sealed class BooleanExt<out T>//定義成協變

object Otherwise : BooleanExt<Nothing>()//Nothing是全部類型的子類型,協變的類繼承關係和泛型參數類型繼承關係一致

class TransferData<T>(val data: T) : BooleanExt<T>()//data只涉及到了只讀的操做

//聲明成inline函數
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {
    this -> {
        TransferData(block.invoke())
    }
    else -> Otherwise
}

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {
    is Otherwise ->
        block()
    is TransferData ->
        this.data
}
複製代碼

5、結語

到這裏Kotlin中有關泛型的全部文章就結束,固然泛型很重要深刻於實際開發各個地方,特別是開發一些框架東西比較多,能夠看到上述Boolean實現就是按照上篇文章教你如何攻克Kotlin中泛型型變的難點(下篇)規則來決定使用哪一種型變類型以及稍加分析下就出來了。總的來講有了那張圖作指導仍是很方便的。其實關於泛型型變,仍是得須要多理解,不能死記規則,只有這樣才能更加靈活運用。最後很是感謝bennyHuo大佬提供的Boolean擴展實現。

Kotlin系列文章,歡迎查看:

原創系列:

翻譯系列:

實戰系列:

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

相關文章
相關標籤/搜索