Kotlin教程(九)泛型

寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻蛛倦,二是可以分享給同樣想學(xué)習(xí)Kotlin的同學(xué)。系列文章的知識(shí)點(diǎn)會(huì)以《Kotlin實(shí)戰(zhàn)》這本書中順序編寫匆浙,在將書中知識(shí)點(diǎn)展示出來同時(shí)刃泌,我也會(huì)添加對(duì)應(yīng)的Java代碼用于對(duì)比學(xué)習(xí)和更好的理解。

Kotlin教程(一)基礎(chǔ)
Kotlin教程(二)函數(shù)
Kotlin教程(三)類勺远、對(duì)象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運(yùn)算符重載及其他約定
Kotlin教程(八)高階函數(shù)
Kotlin教程(九)泛型


泛型類型參數(shù)

泛型允許你定義帶類型形參的類型橙喘,當(dāng)這種類型的實(shí)例被創(chuàng)建出來的時(shí)候,類型形參被替換成稱為類型實(shí)參的具體類型胶逢。例如:

List<String>
Map<String, Person>

和一般類型一樣厅瞎,Kotlin編譯器也常常能推導(dǎo)出類型實(shí)參:

val authors = listOf("Dimtry", "Sevelana")

如果你想創(chuàng)建一個(gè)空的列表,這樣就沒有任何可以推導(dǎo)出類型實(shí)參的線索初坠,你就得顯式地指定它(類型形參)和簸。

val readers: MutableList<String> = mutableListOf()

val readers = mutableListOf<String>()

和Java不同,Kotlin始終要求類型實(shí)參要么被顯式地說明碟刺,要么能被編譯器推導(dǎo)出來锁保。因?yàn)榉盒褪?.5版本才引入到Java的,它必須保證和基于老版本的兼容,所以它允許使用沒有類型參數(shù)的泛型類型——所謂的原生態(tài)類型身诺。而Kotlin從一開始就有泛型蜜托,所以它不支持原生態(tài)類型,類型實(shí)參必須定義霉赡。

泛型函數(shù)和屬性

如果要編寫一個(gè)使用列表的函數(shù)橄务,希望它可以在任何列表上使用,而不是某個(gè)具體類型的元素的列表穴亏,需要編寫一個(gè)泛型函數(shù)蜂挪。

fun <T> List<T>.slice(indices: IntReange): List<T>

基本上是和Java的聲明類似的,在方法名前聲明嗓化,即可在函數(shù)中使用棠涮。

還可以給類或接口的方法,頂層函數(shù)刺覆,擴(kuò)展屬性以及擴(kuò)展函數(shù)聲明類型參數(shù)严肪。例如下面你這個(gè)返回列表倒數(shù)第二個(gè)元素的擴(kuò)展屬性:

val <T> List<T>.penultimate: T
    get() = this[size -2]

不能聲明泛型非擴(kuò)展屬性

普通(非擴(kuò)展)屬性不能擁有類型參數(shù),不能再一個(gè)類的屬性中存儲(chǔ)多個(gè)不同類型的值谦屑,因此聲明泛型非擴(kuò)展函數(shù)函數(shù)沒有任何意義驳糯。

聲明泛型類

和Java一樣,Kotlin通過在類名稱后面加上一對(duì)尖括號(hào)氢橙,并把類型參數(shù)放在尖括號(hào)內(nèi)來聲明泛型類及泛型接口酝枢。一旦聲明之后,就可以在類的主體內(nèi)像其他類型一樣使用類型參數(shù)悍手。

interface List<T> {
    operator fun get(index: Int): T
}

如果你的類繼承了泛型(或者實(shí)現(xiàn)了泛型接口)帘睦,你就得為基礎(chǔ)類型的泛型形參提供一個(gè)類型實(shí)參。

class StringList: List<String> {
    override fun get(index: Int): String = ...
}

類型參數(shù)約束

類型參數(shù)約束可以限制作為(泛型)類和(泛型)函數(shù)的類型實(shí)參的類型坦康。
如果你把一個(gè)類型指定為泛型類型形參的上界約束竣付,在泛型類型具體的初始化中,其對(duì)應(yīng)的類型實(shí)參就必須是這個(gè)具體類型或其子類型涝焙。你是這樣定義約束:把冒號(hào)放在類型參數(shù)名稱之后卑笨,作為類型形參上界的類型緊隨其后:

fun <T : Number> List<T>.sum(): T

相當(dāng)于Java中的:

<T extends Number> T sum(List<T> list)

一旦指定了類型形參T的上界,你就可以把類型T的值當(dāng)做它的上界的值使用:

fun <T : Number> oneHalf(value: T): Double {
    return value.toDouble() //調(diào)用Number的方法
}

極少數(shù)情況下仑撞,需要在一個(gè)類型參數(shù)上指定多個(gè)約束赤兴,這時(shí)你需要使用不同的語法:

fun <T> ensureTrailingPeriod(seq: T) 
    where T : CharSequence, T : Appendable {
    if(!seq.endWith('.') { //調(diào)用CharSequence的方法
        seq.append('.')//調(diào)用Appendable的方法
    }
}

這種情況下,可以說明作為類型實(shí)參的類型必須同時(shí)實(shí)現(xiàn)CharSequence和Appendable兩個(gè)接口隧哮。

讓類型形參非空

如果你聲明的時(shí)泛型類或者泛型函數(shù)桶良,任何類型實(shí)參,包括哪些可空的類型實(shí)參沮翔,都可以替換她的類型形參陨帆。事實(shí)上沒有指定上界的類型形參將會(huì)使用Any? 這個(gè)默認(rèn)上界:

class Processor<T> {
    fun process(value: T) {
        value?.hashCode()
    }
}

process函數(shù)中,參數(shù)value是可空的,盡管T并沒有使用問號(hào)標(biāo)記疲牵。

如果你想保證替換類型形參的始終是非空類型承二,可以通過制定一個(gè)約束來實(shí)現(xiàn)。如果你除了可空性之外沒有任何限制纲爸,可以使用Any代替默認(rèn)的Any?作為上界亥鸠。

class Processor<T : Any> {
    fun process(value: T) {
        value.hashCode()
    }
}

運(yùn)行時(shí)的泛型:擦除和實(shí)化類型參數(shù)

你可能知道,JVM上的泛型一般是通過類型擦除實(shí)現(xiàn)的识啦,就是說泛型類實(shí)例的類型實(shí)參在運(yùn)行時(shí)是不保留的负蚊。

運(yùn)行時(shí)的泛型:類型檢查和轉(zhuǎn)換

和Java一樣,Kotlin的泛型在運(yùn)行時(shí)也被擦除了颓哮。這意味著泛型類實(shí)例不會(huì)攜帶用于創(chuàng)建它的類型實(shí)參的信息家妆。例如,如果你創(chuàng)建一個(gè)List<String>并將一堆字符串放到其中冕茅,在運(yùn)行時(shí)你只能看到它是一個(gè)List伤极,不能識(shí)別出列表本打算包含的時(shí)哪種類型的元素。
隨著擦除類型信息也帶來了約束嵌赠。因?yàn)轭愋蛯?shí)參沒有被存儲(chǔ)下來塑荒,你不能檢查他們熄赡。例如姜挺,你不能判斷一個(gè)列表是一個(gè)包含字符串的列表還是包含其他對(duì)象的列表:

>>> if (value is List<String>)
ERROR: Canot check for instance of erased type

那么如何檢查一個(gè)值是否是列表,而不是set或者其他對(duì)象彼硫〈逗溃可以使用特殊的*投影語法來做這樣的檢查:

if (value is List<*>)

這種表示擁有未知類型實(shí)參的泛型類型,類似于Java中的List<?>拧篮。

注意词渤,在asas?轉(zhuǎn)換中仍然可以使用一般的泛型類型。但是如果該類有正確的基礎(chǔ)類型但類型實(shí)參是錯(cuò)誤的串绩,轉(zhuǎn)換也不會(huì)失敗缺虐,因?yàn)樵谶\(yùn)行時(shí)轉(zhuǎn)換發(fā)生的時(shí)候類型實(shí)參是未知的。因此礁凡,這樣的轉(zhuǎn)換會(huì)導(dǎo)致編譯器發(fā)出“unchecked cast”的警告高氮。這僅僅是一個(gè)警告,你仍然可以繼續(xù)使用這個(gè)值顷牌。

fun printSum(c: Collection<*>) {
    //這里會(huì)有警告:Unchecked cast:List<*> to List<Int>
    val intList = c as? List<Int> 
            ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

>>> printSum(listOf(1, 2, 3))
6

編譯一切正常:編譯器只是發(fā)出了一個(gè)警告剪芍,這意味著代碼是合法的。如果在一個(gè)整型的列表或者set上調(diào)用該函數(shù)窟蓝,一切都會(huì)如預(yù)期發(fā)生:第一種情況會(huì)打印元素之和罪裹,第二種情況會(huì)拋出IllegalArgumentException異常。但如果你傳遞了一個(gè)錯(cuò)誤類型的值,如List<String>状共,運(yùn)行時(shí)會(huì)得到一個(gè)ClassCastException套耕。

聲明帶實(shí)化類型參數(shù)的函數(shù)

前面說過,Kotlin泛型在運(yùn)行時(shí)會(huì)被擦除峡继,泛型函數(shù)的類型實(shí)參也是這樣箍铲。在調(diào)用泛型函數(shù)的時(shí)候,在函數(shù)體中你不能決定調(diào)用它用的類型實(shí)參:

>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T

通常情況下都是這樣的鬓椭,只有一種例外可以避免這種限制:內(nèi)聯(lián)函數(shù)颠猴。內(nèi)聯(lián)函數(shù)的類型形參能夠被實(shí)化,意味著你可以在運(yùn)行時(shí)引用實(shí)際的類型實(shí)參小染。
在之前章節(jié)中翘瓮,我們知道如果用inline關(guān)鍵字標(biāo)記一個(gè)函數(shù)媒殉,編譯器會(huì)把每一次函數(shù)調(diào)用都換成函數(shù)實(shí)際的代碼實(shí)現(xiàn)强挫。使用內(nèi)聯(lián)函數(shù)還可以提升性能贺辰,如果該函數(shù)使用了lambda實(shí)參:lambda的代碼也會(huì)內(nèi)聯(lián)维咸,所以不會(huì)創(chuàng)建任何匿名類便脊∶兜觯基于這種實(shí)現(xiàn)原理渺蒿,應(yīng)該也可以想象到隔节,根據(jù)嵌入的上下文筐带,泛型在class文件中已經(jīng)被確定了今穿。
如果把前面例子中的isA函數(shù)聲明成inline并且用reified標(biāo)記類型參數(shù),你就能夠用該函數(shù)檢查value是不是T的實(shí)例了伦籍。

inline fun <reified T> isA(value: Any) = value is T

>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false

一個(gè)實(shí)化類型參數(shù)能發(fā)揮作用的最簡(jiǎn)單的例子就是標(biāo)準(zhǔn)庫(kù)函數(shù)filterIsInstance 蓝晒。這個(gè)函數(shù)接收一個(gè)集合,選擇其中哪些指定類的實(shí)例帖鸦,然后返回這些被選中的實(shí)例芝薇。

>>> val items = listOf("one", 2, "three")
>>> println(items.filterIsInstance<String>())
[one, three]

通過指定<String>作為函數(shù)的類型實(shí)參,你表明感興趣的只是字符串作儿。因此函數(shù)的返回類型是List<String>洛二。這種情況下,類型實(shí)參在運(yùn)行時(shí)是已知的攻锰,函數(shù)filterIsInstance使用它來檢查列表中的值是不是指定為該類型實(shí)參的類的實(shí)例晾嘶。
下面是Kotlin標(biāo)準(zhǔn)庫(kù)函數(shù)filterIsInstance聲明的簡(jiǎn)化版本:

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
    return destination
}

在之前章節(jié),我們提到把函數(shù)標(biāo)記成內(nèi)聯(lián)只有在一種情況下有性能優(yōu)勢(shì)口注,即函數(shù)擁有函數(shù)類型的形參并且其對(duì)應(yīng)的實(shí)參lambda和函數(shù)一起被內(nèi)聯(lián)的時(shí)候变擒。而現(xiàn)在我們是為了能夠使用實(shí)化參數(shù)而把函數(shù)標(biāo)記成內(nèi)聯(lián)。

為什么實(shí)化只對(duì)內(nèi)聯(lián)函數(shù)有效

編譯器把實(shí)現(xiàn)內(nèi)聯(lián)函數(shù)的字節(jié)碼插入每一次調(diào)用發(fā)生的地方寝志。每次你調(diào)用帶實(shí)化類型參數(shù)的函數(shù)時(shí)娇斑,編譯器都知道這次特定調(diào)用中用作類型實(shí)參的切確類型策添。因此,編譯器可以生成引用作為類型實(shí)參的具體類的字節(jié)碼毫缆。實(shí)際對(duì)filterIsInstance<String>掉用來說唯竹,生成的代碼和下面這段代碼是等價(jià)的:

for (element in this) {
    if (element is String) {
        destination.add(element)
    }
}

因?yàn)樯傻淖止?jié)碼引用了具體類,而不是類型參數(shù)苦丁,它不會(huì)被運(yùn)行時(shí)發(fā)生的類型參數(shù)擦除影響浸颓。
注意,帶reified類型參數(shù)的inline函數(shù)不能再Java代碼中調(diào)用旺拉。 普通內(nèi)聯(lián)函數(shù)可以像常規(guī)函數(shù)那樣在Java中調(diào)用——他們可以被調(diào)用而不能被內(nèi)聯(lián)产上。帶實(shí)化參數(shù)類型的函數(shù)需要額外的處理,來把類型參數(shù)的值替換到字節(jié)碼中蛾狗,所以他們必須永遠(yuǎn)是內(nèi)聯(lián)的晋涣。這樣他們不可能用Java那樣的普通方式調(diào)用。

使用實(shí)化類型參數(shù)代替類引用

如果你是Android開發(fā)者沉桌,顯示Activity是一個(gè)最常用的方法谢鹊。也可以使用實(shí)化類型參數(shù)來代替?zhèn)鬟f作為java.lang.Class的Activity類:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

>>> startActivity

::class.java的語法展現(xiàn)了如何獲取java.lang.Class對(duì)應(yīng)的Kotlin類。這和Java中的Service.class是完全等同的留凭。

實(shí)化類型參數(shù)的限制

盡管實(shí)化類型參數(shù)是方便的工具佃扼,但它們也有一些限制。有一些事實(shí)化與生俱來的蔼夜,而另外一些則是現(xiàn)有的實(shí)現(xiàn)決定的兼耀,而且可能在未來的Kotlin版本中放松這些限制。
具體來說挎扰,可以按下面的方式使用實(shí)化類型參數(shù):

  • 用在類型檢查和類型轉(zhuǎn)換中(is翠订、!isas遵倦、as?
  • 使用Kotlin反射API(::class
  • 獲取相應(yīng)的java.lang.Class::class.java
  • 作為調(diào)用其他函數(shù)的類型實(shí)參

不能做下面的這些事情:

  • 創(chuàng)建指定為類型參數(shù)的類的實(shí)例
  • 調(diào)用類型參數(shù)類的伴生對(duì)象的方法
  • 調(diào)用帶實(shí)化類型參數(shù)函數(shù)的時(shí)候使用非實(shí)化類型形參作為類型實(shí)參
  • 把類、屬性或者非內(nèi)聯(lián)函數(shù)的類型參數(shù)標(biāo)記成reified

變型:泛型和子類型化

變型的概念描述了擁有相同基礎(chǔ)類型和不同類型實(shí)參的(泛型)類型之間是如何關(guān)聯(lián)的:例如官撼,List<String>List<Any>之間如何關(guān)聯(lián)梧躺。

為什么存在變型:給函數(shù)傳遞實(shí)參

假如你有一個(gè)接收List<Any>作為實(shí)參的函數(shù)。把List<String>類型的變量傳給這個(gè)函數(shù)時(shí)候安全傲绣?毫無疑問掠哥,把一個(gè)字符串傳給一個(gè)期望Any的函數(shù)是安全的,因?yàn)镾tring繼承了Any秃诵。但當(dāng)String和Any變成List接口的類型實(shí)參之后续搀,情況就沒有這么簡(jiǎn)單了。

fun printContents(list: List<Any>) {
    println(list.joinToString())
}

>>> printContents(listOf("abc", "bac"))
abc, bac

這看上去沒什么問題菠净,我們來看另一個(gè)例子:

fun addAnswer(list: MutableList<Any>) {
    list.add(42)
}

>>> val strings = mutableListOf("abc", "bac")
>>> addAnswer(strings)
Type mismatch. Required: MutableList<Any> Found: MutableList<String>

這個(gè)例子和上面的例子中禁舷,區(qū)別僅僅是將List<Any>變成了MutableList<Any>彪杉,就無法將泛型為String的list傳遞給函數(shù)。
現(xiàn)在可以回答剛才那個(gè)問題了牵咙,把一個(gè)字符串列表傳給期望Any對(duì)象列表的函數(shù)是否安全派近。如果函數(shù)添加或者替換了列表中的元素就是不安全的,因?yàn)檫@樣會(huì)產(chǎn)生類型不一致的可能性洁桌。在Kotlin中渴丸,可以通過根據(jù)列表是否可變選擇合適的接口來輕松的控制。如果函數(shù)接收的是只讀列表另凌,可以傳遞具有更具體的元素類型的列表谱轨。如果列表是可變的,就不能這么做吠谢。

類碟嘴、類型和子類型

為了討論類型之間的關(guān)系,需要熟悉子類型這個(gè)術(shù)語囊卜。任何時(shí)候如果需要的時(shí)類型A的值娜扇,你都能夠使用類型B的值(當(dāng)做A的值),類型B就稱為類型A的子類型栅组。舉例來說雀瓢,Int是Number的子類型,但I(xiàn)nt不是String的子類型玉掸。這個(gè)定義還標(biāo)明了任何類型都可以被認(rèn)為是它自己的子類型刃麸。
術(shù)語超類型是子類型的反義詞。如果A是B的子類型司浪,那么B就是A的超類型泊业。
為什么一個(gè)類型是否是另一個(gè)的子類型這么重要?編譯器在每一次給變量賦值或者給函數(shù)傳遞實(shí)參的時(shí)候都要做這項(xiàng)檢查啊易。

fun test(i: Int) {
    val n: Number = i  //可以編譯
    fun f(s: String) {/*...*/}
    f(i)  //不能編譯
}

只有值得類型是變量類型的子類型時(shí)吁伺,才允許變量存儲(chǔ)該值。例如租谈,變量n的初始化器i的類型Int是變量的類型Number的子類型篮奄,所以n的聲明是合法的。只有當(dāng)表達(dá)式的類型是函數(shù)參數(shù)的類型的子類型時(shí)割去,才允許把該表達(dá)式傳給函數(shù)窟却。這個(gè)例子中i的類型Int不是函數(shù)參數(shù)的類型String的子類型,所以函數(shù)f的調(diào)用會(huì)編譯失敗呻逆。
你可能認(rèn)為子類型就是子類的概念夸赫,但是為什么在Kotlin中稱之為子類型呢?因?yàn)榭С牵琄otlin存在可空類型茬腿。一個(gè)非空類型是它的可空版本的子類型呼奢,但它們都對(duì)應(yīng)著同一個(gè)類。你始終能在可空類型的變量中存儲(chǔ)非空類型的值滓彰,但反過來卻不行控妻。

var s: String = "abc"
val t: String? = s //編譯通過
s = t  //編譯不通過

前面,我們把List<String>類型的變量傳給期望List<Any>的函數(shù)是否安全揭绑,現(xiàn)在可以使用子類型化術(shù)語來重新組織:List<String>List<Any>的子類型嗎弓候?你已經(jīng)了解了為什么把MutableList<String>當(dāng)成MutableList<Any>的子類型對(duì)待是不安全的。顯然他匪,返回來也是不成立的:MutableList<Any>肯定不是MutableList<String>的子類型菇存。
一個(gè)泛型類(例如MutableList)如果對(duì)于任意兩種類型A和B,MutableList<A>既不是MutableList<B>的子類型也不是他的超類型邦蜜,他就是被稱為在該類型參數(shù)上是不變型的依鸥。Java中所有的類都是不變型的(盡管哪些類具體的使用可以標(biāo)記成可變型的,稍后你就會(huì)看到)悼沈。

List類的類型化規(guī)則不一樣贱迟,Kotlin中的List接口表示的是只讀集合,如果A是B的子類型絮供,那么List<A>就是List<B>的子類型衣吠。這樣的類或者接口被稱為協(xié)變的。

協(xié)變:保留子類型化關(guān)系

一個(gè)協(xié)變類是一個(gè)泛型類(我們以Producer<T>為例)壤靶,對(duì)這種類來說缚俏,下面的描述是成立的:如果A是B的子類型,那么Producer<A>就是Producer<B>的子類型贮乳。我們說子類型化被保留了忧换。
在Kotlin中,要聲明類在某個(gè)類型參數(shù)上是可以協(xié)變的向拆,在該類型參數(shù)的名稱前面加上out關(guān)鍵字即可:

interface Producer<out T> {
    fun produce(): T
}

將一個(gè)類的類型參數(shù)標(biāo)記為協(xié)變得亚茬,在該類型實(shí)參沒有精確匹配到函數(shù)中定義的類型形參時(shí),可以讓該類的值作為這些函數(shù)的實(shí)參傳遞亲铡,也可以作為這些函數(shù)的返回值才写。例如,想象一下有這樣一個(gè)函數(shù)奖蔓,它負(fù)責(zé)喂養(yǎng)用類Herd代表的一群動(dòng)物,Herd類的類型參數(shù)確定了畜群中動(dòng)物的類型讹堤。

open class Animal {
    fun feed() {...}
}

class Herd<T : Animal> {
    val size: Int
        get() = ...

    operator fun get(i: Int): T {...}
}

fun feeAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}

假設(shè)這段代碼的用戶有一群貓需要照顧:

class Cat : Animal() {
    fun cleanLitter() {...}
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // feedAll(cats)  //錯(cuò)誤:類型不匹配
    }
}

如果嘗試把貓群傳給feedAll函數(shù)吆鹤,在編譯期你就會(huì)得到類型不匹配的錯(cuò)誤。因?yàn)镠erd類中的類型參數(shù)T沒有用任何變型修飾符洲守,貓群不是畜群的子類疑务≌雌啵可以使用顯示得類型轉(zhuǎn)換來繞過這個(gè)問題,但是這種方法啰嗦知允、易出錯(cuò)撒蟀,而且?guī)缀鯊膩聿皇墙鉀Q類型不匹配問題的正確方式。
因?yàn)镠erd類有一個(gè)類似List的API温鸽,并且不允許它的調(diào)用者添加和改變畜群中的動(dòng)物保屯,可以把它變成協(xié)變并相應(yīng)地修改調(diào)用代碼。

class Herd<out T: Animal> {
    ...
}

你不能把任何類都變成協(xié)變得:這樣不安全涤垫。讓類在某個(gè)類型參數(shù)變?yōu)閰f(xié)變姑尺,限制了該類中對(duì)該類型參數(shù)使用的可能性。要保證類型安全蝠猬,它只能用在所謂的out位置切蟋,意味著這個(gè)類只能生產(chǎn)類型T的值而不能消費(fèi)它們。
在類成員的聲明中類型參數(shù)的使用可以分為in位置和out位置榆芦”猓考慮這樣一個(gè)類,它聲明了一個(gè)類型參數(shù)T并包含了一個(gè)使用T的函數(shù)匆绣。如果函數(shù)是把T當(dāng)成返回類型驻右,我們說它在out位置。這種情況下犬绒,該函數(shù)生產(chǎn)類型為T的值旺入。如果T用作函數(shù)參數(shù)的類型,它就在in位置凯力,這樣的函數(shù)消費(fèi)類型為T的值茵瘾。

interface Transformer<T> {
                //in位置 //out位置
    fun transform(t: T): T
}

類的類型參數(shù)前的out關(guān)鍵字要求所有使用T的方法只能把T放在out位置而不能放在in位置。這個(gè)關(guān)鍵字約束了使用T的可能性咐鹤,這保證了對(duì)應(yīng)子類型關(guān)系的安全性拗秘。

重申一下,類型參數(shù)T上的關(guān)鍵字out有兩層含義:

  • 子類型化被保留
  • T只能用在out位置

現(xiàn)在我們看看List<Interface>接口祈惶。Kotlin的List是只讀的雕旨,所以它只有一個(gè)返回類型為T的元素的方法get,而沒有定義任何把類型為T的元素存儲(chǔ)到列表中的方法捧请。因此凡涩,它也是協(xié)變的。

interface List<out T> : Collection<T> {
    operator fun get(index: Int): T
}

注意疹蛉,類型形參不光可以直接當(dāng)作參數(shù)類型或者返回類型使用活箕,還可以當(dāng)作另一個(gè)類型的類型實(shí)參。例如可款,List接口就包含了一個(gè)返回List<T>的subList方法:

interface List<out T> : Collection<T> {
    fun subList(fromIndex: Int, toIndex: Int): List<T>
}

在這個(gè)例子中育韩,函數(shù)subList中的T也用在out位置克蚂。
注意,不能把MutableList<T>在它的類型參數(shù)上聲明成協(xié)變的筋讨,因?yàn)樗群薪邮疹愋蜑門的值作為參數(shù)的方法埃叭,也含有返回這種值得方法(因此,T出現(xiàn)在in和out兩種位置上)悉罕。

interface MutableList<T>
        : List<T>, MultableCollection<T> {
    override fun add(element: T): Boolean   
}

編譯器強(qiáng)制實(shí)施了這種限制赤屋。如果這個(gè)類被聲明成協(xié)變得,編譯器會(huì)報(bào)錯(cuò):Type parameter T is declared as 'out' but occurs in 'in' position(類型參數(shù)T聲明為out但出現(xiàn)在in位置)蛮粮。
注意益缎,構(gòu)造方法的參數(shù)即不在in位置,也不在out位置然想。即使類型參數(shù)聲明成了out莺奔,仍然可以在構(gòu)造方法參數(shù)的聲明中使用它:

class Herd<out T: Animal>(vararg animals: T) {...}

如果把類的實(shí)例當(dāng)成一個(gè)更泛化的類型的實(shí)例使用,變型會(huì)防止該實(shí)例被誤用:不能調(diào)用存在潛在危險(xiǎn)的方法变泄。構(gòu)造方法不是那種在實(shí)例創(chuàng)建之后還能調(diào)用的方法令哟,因此它不會(huì)有潛在危險(xiǎn)。
然后妨蛹,如果你在構(gòu)造方法的參數(shù)上使用了關(guān)鍵字val和var屏富,同時(shí)就會(huì)聲明一個(gè)getter和一個(gè)setter(如果屬性是可變的)。因此蛙卤,對(duì)只讀屬性來說狠半,類型參數(shù)用在了out位置,而可變屬性在out位置和in位置都使用了它:

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {...}

上面這個(gè)例子中颤难,T不能用out標(biāo)記神年,因?yàn)轭惏瑢傩詌eadAnimal的setter,它在in位置用到了T行嗤。
還需要注意的是已日,位置規(guī)則只覆蓋了類外部可見的(public、protected和internal)API栅屏。私有方法的參數(shù)即不在in位置也不在out位置飘千。變型規(guī)則只會(huì)防止外部使用者對(duì)類的誤用但不會(huì)對(duì)類自己的實(shí)現(xiàn)起作用:

class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {...}

現(xiàn)在可以安全地讓Herd在T上協(xié)變,因?yàn)閷傩詌eadAnimal變成了私有的栈雳。

逆變:反轉(zhuǎn)子類型化關(guān)系

逆變的概念可以被看成是協(xié)變的鏡像:對(duì)一個(gè)逆變來說护奈,它的子類型化關(guān)系與用作類型實(shí)參的類的子類型化關(guān)系是相反的。我們從Comparator接口的例子開始哥纫,這個(gè)接口定義了一個(gè)方法compare類逆济,用于比較兩個(gè)給定的對(duì)象:

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int {...}
}

一個(gè)為特定類型的值定義的比較器顯然可以比較該類型任意子類型的值。例如磺箕,如果有一個(gè)Comparator<Any>奖慌,可以用它比較任意具體類型的值。

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int
}

fun main(args: Array<String>) {
    val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
    val strings = listOf("a", "b", "c")
    strings.sortedWith(anyComparator)
}

sortedWith函數(shù)期望一個(gè)Comparator<String>(一個(gè)可以比較字符串的比較器)松靡,傳給它一個(gè)能比較更一般的類型的比較器是安全的简僧。如果你要在特定類型的對(duì)象上執(zhí)行比較,可以使用能處理該類型或者它的超類型的比較器雕欺。這說明Comparator<Any>Comparator<String>的子類型岛马,其中Any是String的超類型。不同類型之間的子類型關(guān)系和這些類型的比較器之間的子類型化關(guān)系截然相反屠列。
現(xiàn)在你已經(jīng)為完整的逆變定義做好了準(zhǔn)備啦逆。一個(gè)在類型參數(shù)上逆變的類是這樣的一個(gè)泛型類(我們以Consumer<T>為例),對(duì)這種類來說笛洛,下面的描述是成立的:如果B是A的子類夏志,那么Consumer<A>就是Consumer<B>的子類型,類型參數(shù)A和B交換了位置苛让,所以我們說子類型化被反轉(zhuǎn)了沟蔑。

in關(guān)鍵字的意思是,對(duì)應(yīng)類型的值是傳遞進(jìn)來給這個(gè)類的方法的狱杰,并且被這些方法消費(fèi)瘦材。和協(xié)變得情況類似,約束類型參數(shù)的使用將導(dǎo)致特定的子類型化關(guān)系仿畸。在類型參數(shù)T上的in關(guān)鍵字意味著子類型化被反轉(zhuǎn)了食棕,而且T只能用在in位置。

協(xié)變 逆變 不變型
Producer<out T> Consumer<in T> MutableList<T>
類的子類型化保留了:Producer<Cat>Producer<Animal>的子類型 子類型化反轉(zhuǎn)了:Consumer<Animal>Consumer<Cat>的子類型 沒有子類型化
T 只能在out位置 T只能在in位置 T可以在任何位置

協(xié)變得错沽,逆變的和不變型的類

協(xié)變 逆變 不變型
Producer<out T> Consumer<in T> MutableList<T>
類的子類型化保留了:Producer<Cat>Producer<Animal>的子類型 子類型化反轉(zhuǎn)了:Consumer<Animal>Consumer<Cat>的子類型 沒有子類型化
T 只能在out位置 T只能在in位置 T可以在任何位置

一個(gè)類可以在一個(gè)類型參數(shù)上協(xié)變簿晓,同時(shí)在另外一個(gè)類型參數(shù)上逆變。Function接口就是一個(gè)經(jīng)典的例子甥捺。下面是一個(gè)單個(gè)參數(shù)的Function的聲明:

interface Function1<in P, out R> {
    operator fun invoke(p: P): R
}

Kotlin的表達(dá)發(fā)(P) -> R是表達(dá)Function<P, R>的另一種更具可讀性的形式抢蚀。可以發(fā)現(xiàn)用in關(guān)鍵字標(biāo)記的P(參數(shù)類型)只用在in位置镰禾,而用out關(guān)鍵字標(biāo)記的R(返回類型)只用在out位置皿曲。這意味著對(duì)這個(gè)函數(shù)類型的第一個(gè)類型參數(shù)來說,子類型化反轉(zhuǎn)了吴侦,而對(duì)于第二個(gè)類型參數(shù)來說屋休,子類型化保留了。

fun enumerateCats(f: (Cat) -> Number) {...}
fun Animal.getIndex(): Int = ...
>>> enumerateCats(Animal::getIndex)

在Kotlin中這點(diǎn)代碼是合法的备韧。Animal是Cat的超類型劫樟,而Int是Number的子類型。

使用點(diǎn)變型:在類型出現(xiàn)的地方指定變型

在類聲明的時(shí)候就能夠指定變型修飾符是很方便的,因?yàn)檫@些修飾符會(huì)應(yīng)用到所有類被使用的地方叠艳。這被稱作聲明點(diǎn)變型奶陈。如果你熟悉Java的通配符類型(? extends 和 ? super),你會(huì)意識(shí)到Java用完全不同的方式處理變型附较。在Java中吃粒,每一次使用帶類型參數(shù)的類型的時(shí)候,還可以指定這個(gè)類型參數(shù)是否可以用他的子類型或者超類型替換拒课。這叫做使用點(diǎn)變型徐勃。

Kotlin的聲明點(diǎn)變型 vs. Java通配符

聲明點(diǎn)變形帶來了更簡(jiǎn)潔的代碼,因?yàn)橹挥弥付ㄒ淮巫冃托揎椃缦瘢羞@個(gè)類的使用者都不用再考慮這些了僻肖,在Java中,庫(kù)作者不得不一直使用通配符:Function<? super T, ? extends R>卢鹦,來創(chuàng)建按照用戶期望的運(yùn)行的API臀脏。如果你查看Java 8標(biāo)準(zhǔn)庫(kù)的源碼,你會(huì)在每次用到Function接口的地方發(fā)現(xiàn)通配符法挨。例如谁榜,下面是Stream.map方法的聲明:

/* Java */
public interface Stream<T> {
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

Kotlin也支持使用點(diǎn)變型,允許在類型參數(shù)出現(xiàn)的具體位置指定變型凡纳,即使在類型聲明時(shí)它不能被聲明成協(xié)變或逆變的窃植。
你已經(jīng)見過許多像MutableList這樣的接口,通常情況下即不是協(xié)變也不是逆變的荐糜,因?yàn)樗瑫r(shí)生產(chǎn)和消費(fèi)指定為它們類型參數(shù)的類型的值巷怜。但對(duì)于這個(gè)類型的變量來說,在某個(gè)特定函數(shù)中只被當(dāng)成其中一種角色使用的情況挺常見的:要么是生產(chǎn)者要么是消費(fèi)者暴氏。例如下面這個(gè)簡(jiǎn)單的函數(shù):

fun <T> copyData(source: MutableList<T>, 
        destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}

這個(gè)函數(shù)從一個(gè)集合把元素拷貝到另一個(gè)集合中延塑。盡管兩個(gè)集合都擁有不變型的類型,來源集合只是用于讀取答渔,而目標(biāo)集合只是用于寫入关带。這種情況下,集合元素的類型不需要精確匹配沼撕。例如宋雏,把一個(gè)字符串的集合拷貝到可以包含任意對(duì)象的集合中一點(diǎn)兒?jiǎn)栴}也沒有。
要讓這個(gè)函數(shù)支持不同類型的列表务豺,可以引入第二個(gè)泛型參數(shù)磨总。

fun <T : R, R> copyData(source: MutableList<T>,
                destination: MutableList<R>) {
    for (item in source) {
        destination.add(item)
    }
}

>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems)
>>> println(anyItems)
[1, 2, 3]

你聲明了兩個(gè)泛型參數(shù)代表來源列表和目標(biāo)列表中的元素類型。為了能夠把一個(gè)列表中的元素拷貝到另一個(gè)列表中笼沥,來源元素類型應(yīng)該是目標(biāo)列表中的元素的子類型(Int是Any的子類型)蚪燕。
但是Kotlin提供了一種更優(yōu)雅的表達(dá)方式娶牌。當(dāng)函數(shù)的實(shí)現(xiàn)調(diào)用了那些類型參數(shù)只出現(xiàn)在out位置(或只出現(xiàn)在in位置)的方法時(shí),可以充分利用這一點(diǎn)馆纳,在函數(shù)定義中給特定用途的類型參數(shù)加上變型修飾符诗良。

fun <T> copyData(source: MutableList<out T>,
                 destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}

可以為類型聲明中類型參數(shù)任意的用法指定變型修飾符,這些用法包括:形參類型厕诡、局部變量類型累榜、函數(shù)返回類型,等等灵嫌。這里發(fā)生的一切被稱作類型投影:我們說source不是一個(gè)常規(guī)的MutableList,而是一個(gè)投影(受限)的MutableList葛作。只能調(diào)用返回類型是泛型類型參數(shù)的那些方法寿羞,或者嚴(yán)格的講,只在out位置使用它的方法赂蠢。編譯器禁止調(diào)用使用類型參數(shù)做實(shí)參的那些方法(在in位置使用類型參數(shù)):

>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add (element: E): Boolean'

不要為使用投影類型后不能調(diào)用某些方法而吃驚绪穆,如果需要調(diào)用那些方法,你要用的時(shí)常規(guī)類型而不是投影虱岂。這可能要求你聲明第二個(gè)類型參數(shù)玖院,它依賴的原本要進(jìn)行投影的類型。

當(dāng)然第岖,實(shí)現(xiàn)copyData函數(shù)的正確方式應(yīng)該是使用List<T>作為source實(shí)參的類型难菌,因?yàn)槲覀冎挥昧寺暶髟贚ist中的方法,并沒有用到MutableList中的方法蔑滓,而且List類型參數(shù)的變型在聲明時(shí)就指定了郊酒。但這個(gè)例子對(duì)展示這個(gè)概念依然十分重要,尤其是要記住大多數(shù)的類并沒有像List和MutableList這樣分開的兩個(gè)接口键袱,一個(gè)是協(xié)變的讀取接口燎窘,另一個(gè)是不變型的讀取/寫入接口。
如果類型參數(shù)已經(jīng)有out變型蹄咖,獲取它的out投影沒有任何意義褐健。就像List<out T>這樣。它和List<T>是一個(gè)意思澜汤,因?yàn)長(zhǎng)ist已經(jīng)聲明成了class List<out T>蚜迅。編譯器會(huì)發(fā)出警告,標(biāo)明這樣的投影是多余的银亲。

同理慢叨,可以對(duì)類型參數(shù)的用法使用in修飾符,來表明在這個(gè)特定的地方务蝠,相應(yīng)的值擔(dān)當(dāng)?shù)臅r(shí)消費(fèi)者拍谐,而且類型參數(shù)可以使用它的任意子類型替換。

fun <T> copyData(source: MutableList<T>,
                 destination: MutableList<in T>) {
    for (item in source) {
        destination.add(item)
    }
}

Kotlin的使用點(diǎn)變型直接對(duì)應(yīng)Java的限界通配符。Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一個(gè)意思轩拨。in投影的MutableList<in T>對(duì)應(yīng)到Java的MutableList<? super T>践瓷。

星號(hào)投影:使用*代替類型參數(shù)

本章前面提到類型檢查和轉(zhuǎn)換的時(shí)候,我們提到了一種特殊的星號(hào)投影語法亡蓉,可以用它來標(biāo)明你不知道關(guān)于泛型實(shí)參的任何信息晕翠。例如,一個(gè)包含未知類型的元素的列表用這種語法表示為List<*>】潮簦現(xiàn)在我們深入探討星號(hào)投影的語義淋肾。

首先需要注意的是MutableList<*>MutableList<Any?>不一樣。你確信MutableList<Any?>這種列表包含的時(shí)任意類型的元素爸邢。而另一方面樊卓,MutableList<*>是包含某種特定類型元素的列表,但是你不知道是哪個(gè)類型杠河。這種列表被創(chuàng)建成一個(gè)包含某種特定類型元素的列表碌尔,比如String,而且創(chuàng)建它的代碼期望只包含那種類型的元素券敌。因?yàn)椴恢朗悄膫€(gè)類型唾戚,你不能像列表中寫入任何東西,因?yàn)槟銓懭氲娜魏沃刀伎赡軙?huì)違反調(diào)用代碼的期望待诅。但是從列表中讀取元素是可行的叹坦,因?yàn)槟阈睦镉袛?shù),所有的存儲(chǔ)在列表中的值都能匹配所有Kotlin類型的超類型Any?:

fun main(args: Array<String>) {
    val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
    val chars = mutableListOf('a', 'b', 'c')
    val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
//    unknownElements.add(42) //編譯器禁止調(diào)用這個(gè)方法
    println(unknownElements.first()) //讀取元素是安全的
}

//輸出
a

為什么編譯器會(huì)把MutableList<*>當(dāng)成out投影的類型咱士?在這個(gè)例子的上下文中立由,MutableList<*>投影成了MutableList<out Any?>,當(dāng)你沒有任何元素類型信息的時(shí)候序厉,讀取Any?類型的元素任然是安全的锐膜,但是向列表中寫入元素是不安全的。
Kotlin的MyType<*>相當(dāng)于Java中的MyType<?>弛房。

對(duì)像Consumer<in T>這樣的逆變類型的參數(shù)來說道盏,星號(hào)投影等價(jià)于<in Nothing>。實(shí)際上文捶,在這種星號(hào)投影中無法調(diào)用任何簽名中有T的方法荷逞。如果類型參數(shù)是逆變的,它就只能表現(xiàn)為一個(gè)消費(fèi)者粹排,而且种远,我們之前討論過,你不知道它可以消費(fèi)的到底是什么顽耳。因此坠敷,不能讓它消費(fèi)任何東西妙同。

當(dāng)類型實(shí)參的信息并不重要的時(shí)候,可以使用星號(hào)投影的語法膝迎,不需要使用任何在簽名中引用類型參數(shù)的方法粥帚,或者只是讀取數(shù)據(jù)額不關(guān)心它的具體類型。例如限次,可以實(shí)現(xiàn)一個(gè)接收List<*>做參數(shù)的printFirst函數(shù):

fun printFirst(list: List<*>) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末芒涡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子卖漫,更是在濱河造成了極大的恐慌费尽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懊亡,死亡現(xiàn)場(chǎng)離奇詭異依啰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)店枣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叹誉,“玉大人鸯两,你說我怎么就攤上這事〕せ恚” “怎么了钧唐?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)匠襟。 經(jīng)常有香客問我钝侠,道長(zhǎng),這世上最難降的妖魔是什么酸舍? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任帅韧,我火速辦了婚禮,結(jié)果婚禮上啃勉,老公的妹妹穿的比我還像新娘忽舟。我一直安慰自己,他們只是感情好淮阐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布叮阅。 她就那樣靜靜地躺著,像睡著了一般泣特。 火紅的嫁衣襯著肌膚如雪浩姥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天状您,我揣著相機(jī)與錄音勒叠,去河邊找鬼兜挨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛缴饭,可吹牛的內(nèi)容都是我干的暑劝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼颗搂,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼担猛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丢氢,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤傅联,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后疚察,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒸走,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年貌嫡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了比驻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡岛抄,死狀恐怖别惦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情夫椭,我是刑警寧澤掸掸,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蹭秋,受9級(jí)特大地震影響扰付,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仁讨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一羽莺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陪竿,春花似錦禽翼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至礁哄,卻和暖如春长酗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桐绒。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工夺脾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留之拨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓咧叭,卻偏偏與公主長(zhǎng)得像蚀乔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菲茬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Kotlin 知識(shí)梳理系列文章 Kotlin 知識(shí)梳理(1) - Kotlin 基礎(chǔ)Kotlin 知識(shí)梳理(2) ...
    澤毛閱讀 2,510評(píng)論 0 4
  • 參考: Kotlin 實(shí)戰(zhàn) Java 泛型推薦閱讀:https://www.zhihu.com/question/...
    zhaoyubetter閱讀 27,523評(píng)論 1 10
  • 前言 人生苦多吉挣,快來 Kotlin ,快速學(xué)習(xí)Kotlin婉弹! 什么是Kotlin睬魂? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,211評(píng)論 9 118
  • 泛型 泛型(Generic Type)簡(jiǎn)介 通常情況的類和函數(shù)氯哮,我們只需要使用具體的類型即可:要么是基本類型,要么...
    Tenderness4閱讀 1,419評(píng)論 4 2
  • 最近工作出了一些緊急狀況商佛,我停了日更喉钢,冥想和運(yùn)動(dòng)、十萬字打卡良姆。 007每周一次作業(yè)不得不提交了班會(huì)主持稿及背后的故...
    曠野里的樹兒閱讀 299評(píng)論 4 2