Kotlin修煉指南(四)
Kotlin這門語言極其靈活,這是一把雙刃劍,相比Java,大家寫的都是白話文,不論水平高低,大家基本都是能非常流暢的閱讀彼此的代碼的,但是在使用Kotlin之后,由于大家的Kotlin表達(dá)水平和思維習(xí)慣的不同,就好造成這樣一種情形,「這tm還能這樣寫?」、「這寫的是個(gè)啥?」、「臥槽、牛B」。
所以下面總結(jié)了一些平時(shí)寫Kotlin時(shí),那些跟Java白話文寫的不太一樣的地方,拓展拓展大家的思維,讓開發(fā)者在寫Kotlin代碼的時(shí)候,能夠更加的有Kotlin味兒。
Sealed Class
Sealed Class,聽上去很高端,密封類,實(shí)際上并不難理解,它密封的是邏輯,作用就是可以讓邏輯更加完善、嚴(yán)謹(jǐn)。
舉個(gè)很常見的例子,在網(wǎng)絡(luò)請求中有兩種狀態(tài),Success和Fail。
open?class?Result
class?Success(val?msg:?String)?:?Result()
class?Fail(val?error:?Throwable)?:?Result()
fun?getResult(result:?Result)?=?when?(result)?{
????is?Success?->?result.msg
????is?Fail?->?result.error.message
????else?->?throw?IllegalArgumentException()
}
在判斷的時(shí)候,可以使用when來進(jìn)行判斷,但是必須有else條件,這就導(dǎo)致了網(wǎng)絡(luò)請求的狀態(tài)出來三種狀態(tài),即Success、Fail和else,這樣一不利于邏輯的完整性,也容易在狀態(tài)很多的時(shí)候漏掉一些狀態(tài)的判斷。
所以,Kotlin提供了Sealed Class來解決這個(gè)問題,避免使用when的時(shí)候,出現(xiàn)這種無用的判斷分支。代碼如下所示。
sealed?class?Results
class?Success(val?message:?String)?:?Results()
class?Failure(val?error:?Exception)?:?Results()
fun?getMessage(result:?Results)?=?when?(result)?{
????is?Success?->?println(result.message)
????is?Failure?->?println(result.error.toString())
}
這樣可以在when的時(shí)候通過快捷鍵自動(dòng)羅列所有的場景。
更加復(fù)雜的,還可以使用Sealed Class來創(chuàng)建嵌套的密封邏輯,例如前面的Error中,還可以封裝更為詳細(xì)的Error類型,在這樣的場景下,Sealed Class的優(yōu)勢就能更一步體現(xiàn)出來了,代碼如下所示。
sealed?class?Result?{
????data?class?Success(val?message:?String)?:?Result()
????sealed?class?Error(val?error:?Exception)?:?Result()?{
????????class?SystemError(exception:?Exception)?:?Error(exception)
????????class?AuthError(exception:?Exception)?:?Error(exception)
????}
????object?NoResponse?:?Result()
}
fun?getMessage(result:?Result)?=?when?(result)?{
????is?Result.Success?->?println(result.message)
????is?Result.Error.SystemError?->?println(result.error)
????is?Result.Error.AuthError?->?println(result.error)
????Result.NoResponse?->?println(result)
}
在寫了when函數(shù)之后,只要判斷的條件是一個(gè)Sealed Class,那么都可以通過快捷鍵自動(dòng)補(bǔ)全,生成所有的枚舉條件,這可比你自己去列舉靠譜多了,特別是像這種嵌套的情況。
在Android中,除了網(wǎng)絡(luò)請求這種比較常用的場景外,View的點(diǎn)擊的封裝,也是比較常用的例子。
例如一個(gè)RecyclerView Item的點(diǎn)擊事件,可以封裝一個(gè)ItemClick的Sealed Class,這個(gè)類中密封了ShareClick,F(xiàn)avoriteClick,DelClick等邏輯,通過設(shè)置點(diǎn)擊監(jiān)聽,handle不同的點(diǎn)擊事件。
Sealed Class的核心就是,用一組清晰明確的類型,將結(jié)果分配給每個(gè)密封狀態(tài),在保存邏輯的嚴(yán)謹(jǐn)性的同時(shí),減少垃圾代碼的產(chǎn)生。
操作符重載
操作符重載可以讓開發(fā)者在原本沒有操作符功能的函數(shù)中,為其新增操作符含義的功能。
操作符重載是各種騷操作的來源,更是一些別有用心者的萬惡之源
例如官方給出的例子,利用 plus (+) 和 minus (-) 對Map集合做加減運(yùn)算,如圖所示。

代碼如下所示。
fun?main()?{
????val?numbersMap?=?mapOf("one"?to?1,?"two"?to?2,?"three"?to?3)
????//?plus?(+)
????println(numbersMap?+?Pair("four",?4))?//?{one=1,?two=2,?three=3,?four=4}
????println(numbersMap?+?Pair("one",?10))?//?{one=10,?two=2,?three=3}
????println(numbersMap?+?Pair("five",?5)?+?Pair("one",?11))?//?{one=11,?two=2,?three=3,?five=5}
????//?minus?(-)
????println(numbersMap?-?"one")?//?{two=2,?three=3}
????println(numbersMap?-?listOf("two",?"four"))?//?{one=1,?three=3}
}
集合中本沒有「+」、「-」操作,但是可以通過重載操作符,給集合類型的變量增加這樣的功能,這樣寫起來更加方便,除了常見的「+」、「-」操作以外,下面這些操作符都可以被重載。

那么重載操作符到底是怎么實(shí)現(xiàn)的呢?Java中好像并沒有這種功能,所以,Kotlin一定是通過編譯器的黑魔法來實(shí)現(xiàn)的,通過反編譯Kotlin的代碼,可以發(fā)現(xiàn)這個(gè)黑魔法。例如上面Map的plus重載運(yùn)算符,在反編譯之后的代碼如下所示。

很明顯,Kotlin就是在編譯的時(shí)候,把重載的操作符替換成了前面定義的函數(shù),實(shí)際上有點(diǎn)類似拓展函數(shù)的實(shí)現(xiàn),所以Java其實(shí)本身不支持重載操作符,但是Kotlin通過編譯器來實(shí)現(xiàn)了操作符的重載。
拓展in的操作符
in操作符具有很強(qiáng)的語義性,所以在自定義的類中,重載in操作符,可以簡化很多操作,特別是在when條件判斷中,例如在Collection中,Kotlin就重載了in操作符,提供了更加方便的判斷,代碼如下所示。
fun?main()?{
????when?(val?input?=?"xuyisheng")?{
????????in?listOf("xuyisheng",?"zhujia")?->?println("result?$input")
????????in?setOf("zj",?"rkk")?->?println("result?$input")
????????else?->?println("result?not?found")
????}
}
那么我們可以模仿Kotlin官方的做法,在自定義的類中重載in操作符,例如給正則增加in操作符,用來判斷匹配類型,代碼如下所示。
operator?fun?Regex.contains(text:?CharSequence):?Boolean?{
????return?this.containsMatchIn(text)
}
fun?main()?{
????when?(val?input?=?"abc")?{
????????in?Regex("[0–9]")?->?println("contains?a?number")
????????in?Regex("[a-zA-Z]")?->?println("contains?a?letter")
????}
}
通過這種方式,語義更加明確,代碼也更加簡潔。
操作符重載一定要慎用,防止有些人重載「+」為「-」,導(dǎo)致代碼難以理解。
集合操作
在Kotlin中,集合有兩種類型,即Collection和Sequence,在Java中,我們很少提及有兩種集合類型,以至于在寫Kotlin的時(shí)候,對它提供的這兩種集合類型傻傻分不清楚。但在Kotlin的函數(shù)式編程世界里,它們的區(qū)別是非常大的。
立即執(zhí)行 (eagerly) 的Collection類型
Collection,是我們最長用的集合類型,甚至成了集合的代名詞,它的特點(diǎn)如下。
每次操作時(shí)立即執(zhí)行的,執(zhí)行結(jié)果會(huì)被存儲(chǔ)到一個(gè)新的集合中 Collection中的轉(zhuǎn)換操作是內(nèi)聯(lián)函數(shù)。例如map函數(shù)的實(shí)現(xiàn)方式,它是一個(gè)創(chuàng)建了新ArrayList的內(nèi)聯(lián)函數(shù),如下圖所示。

這也是通常在使用Collection的函數(shù)式編程方式時(shí),內(nèi)存使用更大的原因。
延遲執(zhí)行 (lazily) 的Sequence類型
Sequence,也是集合的一種,但是被Collection搶了翻譯,所以只能叫做序列,它跟Collection最大的區(qū)別就是,Sequence是延遲執(zhí)行的。
它有兩種類型: 中間操作 (intermediate) 和末端操作 (terminal)。中間操作不會(huì)立即執(zhí)行,它們只是被存儲(chǔ)起來,僅當(dāng)末端操作被調(diào)用時(shí),才會(huì)按照順序在每個(gè)元素上執(zhí)行中間操作,然后執(zhí)行末端操作。
中間操作 (比如 map、distinct、groupBy 等) 會(huì)返回另一個(gè)Sequence,而末端操作 (比如 first、toList、count 等) 則不會(huì)。
同樣是map函數(shù),在Sequence中,像map這樣的中間操作是將轉(zhuǎn)換函數(shù)會(huì)存儲(chǔ)在一個(gè)新的Sequence實(shí)例中,如圖所示。

而例如first這樣的末端操作,則會(huì)真正執(zhí)行具體的操作。例如first,則會(huì)對Sequence中的元素進(jìn)行遍歷,直到找到預(yù)置條件匹配為止,代碼執(zhí)行如下所示。

下面通過一個(gè)例子來演示下這兩種集合類型的操作異同。
data?class?People(val?name:?String,?val?age:?Int)
val?xuyisheng?=?People("xuyisheng",?18)
val?zhujia?=?People("zhujia",?3)
val?rkk?=?People("rkk",?28)
val?zj?=?People("zj",?38)
val?list?=?listOf(xuyisheng,?zhujia,?rkk,?zj)
fun?main()?{
????val?testCollection?=?list.map?{
????????it.copy(age?=?1)
????}.first?{
????????it.name?==?"xuyisheng"
????}
????println(testCollection)
????val?testSequence?=?list.asSequence().map?{
????????it.copy(age?=?1)
????}.first?{
????????it.name?==?"xuyisheng"
????}
????println(testSequence)
}
首先,我創(chuàng)建了一個(gè)List,默認(rèn)為Collection類型,通過asSequence函數(shù),可以將其轉(zhuǎn)換為Sequence。下面分別針對這兩種方式來看下具體的代碼執(zhí)行的流程。
Collections執(zhí)行過程
調(diào)用map函數(shù)時(shí)會(huì)創(chuàng)建一個(gè)新的ArrayList。Kotlin會(huì)遍歷初始Collection中所有項(xiàng)目,并復(fù)制原始的對象,并將每個(gè)元素的age值改為1,再將其添加到新創(chuàng)建的列表中。
調(diào)用first函數(shù)時(shí),會(huì)遍歷每一個(gè)元素,直到找到第一個(gè)符合條件的元素。
Sequences執(zhí)行過程
調(diào)用asSequence函數(shù)創(chuàng)建一個(gè)基于原始集合的迭代器創(chuàng)建一個(gè)Sequence。 調(diào)用map函數(shù),這是一個(gè)中間操作,所以Sequence會(huì)將轉(zhuǎn)換操作的信息存儲(chǔ)到一個(gè)列表中,該列表只會(huì)存儲(chǔ)要執(zhí)行的操作,但并不會(huì)執(zhí)行這些操作。 調(diào)用first函數(shù)時(shí),這是一個(gè)末端操作,所以它會(huì)將中間操作作用到集合中的每個(gè)元素。我們遍歷初始集合和之前存儲(chǔ)的操作列表,對每個(gè)元素執(zhí)行map操作,然后繼續(xù)執(zhí)行first操作,當(dāng)遍歷到符合條件的數(shù)據(jù)時(shí),就完成了操作,所以就無需在剩余的元素中進(jìn)行map操作了。
綜上所述,它們的差異如下。
使用Sequence是不會(huì)去創(chuàng)建中間集合的,但會(huì)創(chuàng)建中間操作集合,在執(zhí)行末端操作時(shí),由于Item會(huì)被逐個(gè)執(zhí)行,所以中間操作只會(huì)作用到部分Item上。
Sequence每個(gè)元素被依次驗(yàn)證,Collection每個(gè)操作都將作用在整個(gè)集合,每個(gè)操作都將創(chuàng)建新的集合。
Collection會(huì)為每個(gè)轉(zhuǎn)換操作創(chuàng)建一個(gè)新的集合,而Sequence僅僅是保留對轉(zhuǎn)換函數(shù)的引用。
Collection的操作使用了內(nèi)聯(lián)函數(shù),所以處理所用到的字節(jié)碼以及傳遞給它的lambda字節(jié)碼都會(huì)進(jìn)行內(nèi)聯(lián)操作。而Sequence不使用內(nèi)聯(lián)函數(shù),因此,它會(huì)為每個(gè)操作創(chuàng)建新的Function對象。
使用場景
針對Collection和Sequence的這種差異,我們需要在不同的場景下,選擇不同的集合類型。
數(shù)據(jù)量小的時(shí)候,其實(shí)Collection和Sequence的使用并無差異 數(shù)據(jù)量大的時(shí)候,由于Collection的操作會(huì)不斷創(chuàng)建中間態(tài),所以會(huì)消耗過多資源,這時(shí)候,就需要采用Sequence了 對集合的函數(shù)式操作太大,例如需要對集合做map、filter、find等等操作,同樣是使用Sequence更高效
