Kotlin 泛型型變的應(yīng)用
上篇文章我們一起研究了 Kotlin 泛型中型變,而這篇文章主要是為了補(bǔ)充泛型中的星投影以及泛型型變是如何被應(yīng)用到實(shí)際開(kāi)發(fā)中的去。并且我會(huì)用上篇文章知識(shí)一步步確定最終我們?cè)撌褂脜f(xié)變、逆變、還是不變,在最后會(huì)用一個(gè)實(shí)際例子來(lái)說(shuō)明。
1. 聲明點(diǎn)變型與使用點(diǎn)變型進(jìn)行對(duì)比
1.1 聲明點(diǎn)變型和使用點(diǎn)變型定義區(qū)別
首先,解釋下什么是聲明點(diǎn)變型和使用點(diǎn)變型,聲明點(diǎn)變型顧名思義就是在定義聲明泛型類的時(shí)候指明型變類型 (協(xié)變、逆變、不變),在 Kotlin 上表現(xiàn)形式就是在聲明泛型類時(shí)候在泛型形參前面加 in 或 out 修飾。 使用點(diǎn)變型就是在每次使用該泛型類的時(shí)候都要去明確指出型變關(guān)系,如果你對(duì) Java 中型變熟悉的話,Java 就是使用了使用點(diǎn)變型。
1.2 兩者優(yōu)點(diǎn)對(duì)比
聲明點(diǎn)變型:
有個(gè)明顯優(yōu)點(diǎn)就是只需要在泛型類聲明時(shí)定義一次型變對(duì)應(yīng)關(guān)系就可以了,那么之后不管在任何地方使用它都不用顯示指定型變對(duì)應(yīng)關(guān)系,而使用點(diǎn)變型就是每處使用的地方都得重復(fù)定義一遍特別麻煩 (又找到一處 Kotlin 優(yōu)于 Java 的地方)。
使用點(diǎn)變型:
實(shí)際上使用點(diǎn)變型也是有使用場(chǎng)景的,可以使用的更加靈活;所以 Kotlin 并沒(méi)有完全摒棄這個(gè)語(yǔ)法點(diǎn),下面會(huì)專門介紹它的使用場(chǎng)景。
1.3 使用對(duì)比
剛剛說(shuō)使用點(diǎn)變型特別麻煩,一起來(lái)看看到底有多麻煩。這里就是以 Java 為代表,我們都知道 Java 中要使用型變,是利用?通配符加 (super/extends) 來(lái)達(dá)到目的,例如: Function<? super T, ? extends E>
, 其中的 ? extends E
就是對(duì)應(yīng)了協(xié)變,而 ? super T
對(duì)應(yīng)的是逆變。這里以 Stream API 中的 flatMap 函數(shù)源碼為例:
@FunctionalInterface
public interface Function<T, R> {//聲明處就不用指定型變關(guān)系
...
}
//可以看到使用點(diǎn)變型非常麻煩,定義一個(gè)mapper的Function泛型類參數(shù)時(shí),還需要指明后面一大串Function<? super T, ? extends Stream<? extends R>>
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
聲明點(diǎn)變型到底有多方便,這里就以 Kotlin 為例,Kotlin 使用 in,out 來(lái)實(shí)現(xiàn)型變對(duì)應(yīng)規(guī)則。這里以 Sequences API 中的 flapMap 函數(shù)源碼為例:
public interface Sequence<out T> {//Sequence定義處聲明了out協(xié)變
/**
* 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聲明了協(xié)變,所以flatMap函數(shù)Sequence中的泛型實(shí)參R就不用再次指明型變類型了
return FlatteningSequence(this, transform, { it.iterator() })
}
通過(guò)以上源碼對(duì)比,明顯看出 Kotlin 中的聲明點(diǎn)變型要比 Java 中的使用點(diǎn)變型要簡(jiǎn)單得多吧。但是呢使用點(diǎn)變型并不是一無(wú)是處,它在 Kotlin 中還是有一定的使用場(chǎng)景的。下面即將揭曉。
2. 如何使用 Kotlin 中的使用點(diǎn)變型
實(shí)際上使用點(diǎn)變型在 Kotlin 中還是有一定的使用場(chǎng)景,想象一下這樣一個(gè)實(shí)際場(chǎng)景,盡管某個(gè)泛型類是不變的,也就是具有可讀可寫的操作,可是有時(shí)候在某個(gè)函數(shù)中,我們一般僅僅只用到只讀或只寫操作,這時(shí)候利用使用點(diǎn)變型它能使一個(gè)不變型的縮小型變范圍蛻化成協(xié)變或逆變的。是不是突然懵逼了,用源碼來(lái)說(shuō)話,你就明白了,一起來(lái)看個(gè)源碼中的例子。
Kotlin 中的 MutableCollection<E>
是不變的,一起來(lái)看下它的定義:
public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {//沒(méi)有in和out修飾,說(shuō)明是不變
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 函數(shù)的源碼定義:
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>聲明成逆變的了,是不是很奇怪啊,之前明明有說(shuō)它是不變的啊,怎么這里就聲明逆變了
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
}
通過(guò)上面的函數(shù)是不是發(fā)現(xiàn)和 MutableCollection 不變相違背啊,實(shí)際上不是的。這里就是一種典型的使用點(diǎn)變型的使用,我們可以再仔細(xì)分析下這個(gè)函數(shù),destination 在 filterTo 函數(shù)的內(nèi)部只做了寫操作,遍歷 Iterable 中的元素,并把他們 add 操作到 destination 集合中,可以驗(yàn)證我們上述的結(jié)論了。
雖然 MutableCollection 是不變的,但是在函數(shù)內(nèi)部只涉及到寫操作,完全就可以使用 使用點(diǎn)變型將它指定成一個(gè)逆變的型變類型,由不變退化成逆變明顯不會(huì)影響泛型安全所以這里處理是完全合法的??梢栽偃タ雌渌喜僮?API,很多地方都使用了這種方式。
上述關(guān)于不變退化到逆變的,這里再講個(gè)不變退化到協(xié)變的例子:
//可以看到source集合泛型類型聲明成了out協(xié)變了???
fun <T> copyList(source: MutableList<out T>, destination: MutableList<T>): MutableList<T>{
for (element in source) destination.add(element)
}
MutableList<E>
就是前面常說(shuō)的不變的類型,同樣具有可讀可寫操作,但是這里的 source 的集合泛型類型聲明成了 out 協(xié)變,會(huì)不會(huì)又蒙了。
應(yīng)該不會(huì)啊,有了之前逆變的例子,應(yīng)該大家都猜到為什么了。很簡(jiǎn)單就是因?yàn)樵?copyList 函數(shù)中,source 集合沒(méi)有涉及寫操作只有讀操作,所以可以使用 使用點(diǎn)變型將 MutableList 的不變型退化成協(xié)變型,而且很顯然不會(huì)引入泛型安全的問(wèn)題。
所以經(jīng)過(guò)上述例子和以前例子關(guān)于如何使用逆變、協(xié)變、不變。還是我之前說(shuō)那句話,不要去死記規(guī)則,關(guān)鍵在于使用場(chǎng)景中讀寫操作是否引入泛型類型安全的問(wèn)題。如果明確讀寫操作的場(chǎng)景了完全可以按照上述例子那樣靈活運(yùn)用泛型的型變的,可以程序?qū)懙酶油昝馈?/p>
3. Kotlin 泛型中的星投影
3.1 星投影的定義
星投影是一種特殊的星號(hào)投影,它一般用來(lái)表示不知道關(guān)于泛型實(shí)參的任何信息,換句話說(shuō)就是它表示一種特定的類型,但是只是這個(gè)類型不知道或者不能被確定而已。
3.2 MutableList<*>
和 MutableList<Any?>
區(qū)別
首先我們需要注意和明確的一點(diǎn)就是 MutableList<*>
和 MutableList<Any?>
是不一樣的,MutableList<*>
表示包含某種特定類型的集合;而 MutableList<Any?>
則是包含任意類型的集合。特定類型集合只不過(guò)不太確定是哪種類型,任意類型表示包含了多種類型,區(qū)別在于特定集合類型一旦確定類型,該集合只能包含一種類型;而任意類型就可以包含多種類型了。
3.3 MutableList<*>
實(shí)際上一個(gè) out 協(xié)變投影
MutableList<*>
實(shí)際上是投影成 MutableList<out Any?>
類型:
我們來(lái)分析下為什么會(huì)這樣投影,我們知道 MutableList<*>
只包含某種特定類型的集合,可能是 String、Int 或者其他類型中的一種,可想而知對(duì)于該集合操作需要禁止寫操作,不能往該集合中寫入數(shù)據(jù),無(wú)法確定該集合的特定類型,寫操作很可能引入一個(gè)不匹配類型到集合中,這是一件很危險(xiǎn)的事。
但是反過(guò)來(lái)想下,如果該集合存在只讀操作,讀出數(shù)據(jù)元素類型雖然不知道,但是始終是安全的。只存在讀操作那么說(shuō)明是協(xié)變,協(xié)變就會(huì)存在保留子類型化關(guān)系,也就是讀出數(shù)據(jù)元素類型是不確定類型子類型,那么可想而知它只替換 Any?
類型的超類型,因?yàn)?Any?
是所有類型的超類型,那么保留型化關(guān)系,所以 MutableList<*>
實(shí)際上就是 MutableList<out Any?>
的子類型了。
4. 一個(gè)實(shí)際例子應(yīng)用泛型型變 (Boolean 擴(kuò)展)
4.1 為什么開(kāi)發(fā)一個(gè) Boolean 擴(kuò)展
給出一個(gè)例子場(chǎng)景,判斷一堆數(shù)集合中是否全是奇數(shù),如果全是返回輸出 "奇數(shù)集合",如果不是請(qǐng)輸出 "不是奇數(shù)集合",首先問(wèn)下大家是否寫過(guò)一下類似下面代碼:
//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("奇數(shù)集合");
return;
}
System.out.println("不是奇數(shù)集合");
}
//kotlin版寫法
fun isOddList() = println(if(numberList.filter{ it % 2 == 1}.count().equals(numberList.size)){"奇數(shù)集合"} else {"不是奇數(shù)集合"})
//Boolean擴(kuò)展版本寫法
fun isOddList() = println(numberList
.filter{ it % 2 == 1 }
.count()
.equals(numberList.size)
.yes{"奇數(shù)集合"}
.otherwise{"不是奇數(shù)集合"})//有沒(méi)有發(fā)現(xiàn)Boolean擴(kuò)展這種鏈?zhǔn)秸{(diào)用更加絲滑
對(duì)比發(fā)現(xiàn),雖然 Kotlin 中的 if-else 表達(dá)式自帶返回值的,但是 if-else 的結(jié)構(gòu)會(huì)打斷鏈?zhǔn)秸{(diào)用,但是如果使用 Boolean 擴(kuò)展,完全可以使你的鏈?zhǔn)秸{(diào)用更加絲滑順暢一路調(diào)用到底。
4.2 Boolean 擴(kuò)展使用場(chǎng)景
Boolean 擴(kuò)展的使用場(chǎng)景個(gè)人認(rèn)為有兩個(gè):
- 配合函數(shù)式 API 一起使用,遇到 if-else 判斷的時(shí)候建議使用 Boolean 擴(kuò)展,因?yàn)樗粫?huì)像 if-else 結(jié)構(gòu)一樣會(huì)打斷鏈?zhǔn)秸{(diào)用的結(jié)構(gòu);
- 另一場(chǎng)景就是 if 的判斷條件組合很多,如果在外層再包裹一個(gè) if 代碼顯得更加臃腫了,此時(shí)使用 Boolean 會(huì)使代碼更簡(jiǎn)潔。
4.3 Boolean 代碼實(shí)現(xiàn)
通過(guò)觀察上述 Boolean 擴(kuò)展的使用,我們首先需要明確幾點(diǎn):
- 我們知道
yes、otherwise
實(shí)際上就是兩個(gè)函數(shù),為什么能鏈?zhǔn)芥溄悠饋?lái)說(shuō)明中間肯定有一個(gè)類似橋梁作用的中間類型作為函數(shù)的返回值類型; - yes、otherwise 函數(shù)的作用域是帶返回值的,例如上述例子它能直接返回字符串類型的數(shù)據(jù);
- yes、oterwise 函數(shù)的都是一個(gè) lamba 表達(dá)式,并且這個(gè) lambda 表達(dá)式將最后表達(dá)式中的值返回;
- yes 函數(shù)是在 Boolean 類型調(diào)用,所以需要基于 Boolean 類型的實(shí)現(xiàn)擴(kuò)展函數(shù)。
那么根據(jù)以上得出幾點(diǎn)特征基本可以把這個(gè)擴(kuò)展的簡(jiǎn)單版本寫出來(lái)了 (暫時(shí)不支持帶返回值的):
//作為中間類型,實(shí)現(xiàn)鏈?zhǔn)芥溄?/span>
sealed class BooleanExt
object Otherwise : BooleanExt()
object TransferData : BooleanExt()
fun Boolean.yes(block: () -> Unit): BooleanExt = when {
this -> {
block.invoke()
TransferData//由于返回值是BooleanExt,所以此處也需要返回一個(gè)BooleanExt對(duì)象或其子類對(duì)象,故暫且定義TransferData object繼承BooleanExt
}
else -> {//此處為else,那么需要鏈接起來(lái),所以需要返回一個(gè)BooleanExt對(duì)象或其子類對(duì)象,故定義Otherwise object繼承BooleanExt
Otherwise
}
}
//為了鏈接起otherwise方法操作所以需要寫一個(gè)BooleanExt類的擴(kuò)展
fun BooleanExt.otherwise(block: () -> Unit) = when (this) {
is Otherwise -> block.invoke()//判斷此時(shí)子類,如果是Otherwise子類執(zhí)行block
else -> Unit//不是,則直接返回一個(gè)Unit即可
}
fun main(args: Array<String>) {
val numberList: List<Int> = listOf(1, 2, 3)
//使用定義好的擴(kuò)展
(numberList.size == 3).yes {
println("true")
}.otherwise {
println("false")
}
}
上述的簡(jiǎn)單版基本上把擴(kuò)展的架子搭出來(lái)但是呢,唯一沒(méi)有實(shí)現(xiàn)返回值的功能,加上返回值的功能,這個(gè)最終版本的 Boolean 擴(kuò)展就實(shí)現(xiàn)了。
現(xiàn)在來(lái)改造一下原來(lái)的版本,要實(shí)現(xiàn)返回值那么 block 函數(shù)不能再返回 Unit 類型,應(yīng)該要返回一個(gè)泛型類型,還有就是 TransferData 不能使用 object 對(duì)象表達(dá)式類型,因?yàn)樾枰脴?gòu)造器傳入泛型類型的參數(shù),所以 TransferData 用普通類替代就好了。
關(guān)于是定義成協(xié)變、逆變還是不變型,我們可以借鑒上篇文章使用到流程選擇圖和對(duì)比表格
將從基本結(jié)構(gòu)形式、有無(wú)子類型化關(guān)系 (保留、反轉(zhuǎn))、有無(wú)型變點(diǎn) (協(xié)變點(diǎn) out、逆變點(diǎn) in)、角色 (生產(chǎn)者輸出、消費(fèi)者輸入)、類型形參存在的位置 (協(xié)變就是修飾只讀屬性和函數(shù)返回值類型;逆變就是修飾可變屬性和函數(shù)形參類型)、表現(xiàn)特征 (只讀、可寫、可讀可寫) 等方面進(jìn)行對(duì)比
協(xié)變 | 逆變 | 不變 | |
---|---|---|---|
基本結(jié)構(gòu) | Producer<out E> |
Consumer<in T> |
MutableList<T> |
子類型化關(guān)系 | 保留子類型化關(guān)系 | 反轉(zhuǎn)子類型化關(guān)系 | 無(wú)子類型化關(guān)系 |
有無(wú)型變點(diǎn) | 協(xié)變點(diǎn) out | 逆變點(diǎn) in | 無(wú)型變點(diǎn) |
類型形參存在的位置 | 修飾只讀屬性類型和函數(shù)返回值類型 | 修飾可變屬性類型和函數(shù)形參類型 | 都可以,沒(méi)有約束 |
角色 | 生產(chǎn)者輸出為泛型形參類型 | 消費(fèi)者輸入為泛型形參類型 | 既是生產(chǎn)者也是消費(fèi)者 |
表現(xiàn)特征 | 內(nèi)部操作只讀 | 內(nèi)部操作只寫 | 內(nèi)部操作可讀可寫 |
第一步:首先根據(jù)類型形參存在位置以及表現(xiàn)特征確定:
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處于函數(shù)返回值位置
this -> {
TransferData(block.invoke())
}
else -> Otherwise//注意: 此處是編譯不通過(guò)的
}
inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {//T處于函數(shù)返回值位置
is Otherwise ->
block()
is TransferData ->
this.data
}
通過(guò)以上代碼我們可以基本確定是協(xié)變或者不變,
第二步:判斷是否存在子類型化關(guān)系:
由于 yes 函數(shù) else 分支返回的是 Otherwise 編譯不通過(guò),很明顯此處不是不變的,因?yàn)樯鲜龃a就是按照不變方式來(lái)寫的。所以基本確定就是協(xié)變。
然后接著改,首先將 sealed class BooleanExt<T>
改為 sealed class BooleanExt<out T>
協(xié)變聲明,然后發(fā)現(xiàn) Otherwise 還是報(bào)錯(cuò),為什么報(bào)錯(cuò)啊,報(bào)錯(cuò)原因是因?yàn)?yes 函數(shù)要求返回一個(gè) BooleanExt<T>
類型,而此時(shí)返回 Otherwise 是個(gè) BooleanExt<Any?>()
,反證法,假如上述是合理,那么也就是 BooleanExt<Any?>
要替代 BooleanExt<T>
出現(xiàn)的地方,BooleanExt<Any?>
是 BooleanExt<T>
子類型,由于 BooleanExt<T>
協(xié)變的,保留子類型型化關(guān)系也就是 Any?
是 T
子類型,明顯不對(duì)吧?
我們都知道 Any?
是所有類型的超類型。所以原假設(shè)明顯不成立,所以編譯錯(cuò)誤很正常,那么逆向思考下,我是不是只要把 Any?
位置用所有的類型的子類型 Nothing
來(lái)替換不就符合了嗎,那么我們自然而然就想到 Nothing
,在 Kotlin 中 Nothing
是所有類型的子類型。所以最終版本 Boolean 擴(kuò)展代碼如下
sealed class BooleanExt<out T>//定義成協(xié)變
object Otherwise : BooleanExt<Nothing>()//Nothing是所有類型的子類型,協(xié)變的類繼承關(guān)系和泛型參數(shù)類型繼承關(guān)系一致
class TransferData<T>(val data: T) : BooleanExt<T>()//data只涉及到了只讀的操作
//聲明成inline函數(shù)
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. 總結(jié)
到這里 Kotlin 中有關(guān)泛型的所有內(nèi)容就結(jié)束了,當(dāng)然泛型很重要,可以深入于實(shí)際開(kāi)發(fā)各個(gè)方面,特別是開(kāi)發(fā)一些框架的時(shí)候用的比較多。其實(shí)關(guān)于泛型型變,還是得需要多理解,不能死記規(guī)則,只有這樣才能更加靈活運(yùn)用。