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