近來(lái)關(guān)于 Kotlin 的文章著實(shí)不少,Google 官方的支持讓越來(lái)越多的開(kāi)發(fā)者開(kāi)始關(guān)注 Kotlin。不久前加入的項(xiàng)目用的是 Kotlin 與 Java 混合開(kāi)發(fā)的模式,紙上得來(lái)終覺(jué)淺,終于可以實(shí)踐一把新語(yǔ)言。 本文就來(lái)小談一下 Kotlin 中的空處理。
一、上手的確容易
先扯一扯 Kotlin 學(xué)習(xí)本身。
之前各種聽(tīng)人說(shuō)上手容易,但真要切換到另一門語(yǔ)言,難免還是會(huì)躊躇是否有這個(gè)必要。現(xiàn)在因?yàn)楣ぷ麝P(guān)系直接上手 Kotlin,感受是 真香(上手的確容易) 。
首先在代碼閱讀層面,對(duì)于有 Java 基礎(chǔ)的程序員來(lái)說(shuō)閱讀 Kotlin 代碼基本無(wú)障礙,除去一些操作符、一些順序上的變化,整體上可以直接閱讀。
其次在代碼編寫層面,僅需要改變一些編碼習(xí)慣。主要是:語(yǔ)句不要寫分號(hào)、變量需要用 var 或 val 聲明、類型寫在變量之后、實(shí)例化一個(gè)對(duì)象時(shí)不用 “new” …… 習(xí)慣層面的改變只需要多寫代碼,自然而然就適應(yīng)了。
最后在學(xué)習(xí)方式層面,由于 Kotlin 最終都會(huì)被編譯成字節(jié)碼跑在 JVM 上,所以初入手時(shí)完全可以用 Java 作為對(duì)比。比如你可能不知道 Kotlin 里 companion object 是什么意思,但你知道既然 Kotlin 最終會(huì)轉(zhuǎn)成 jvm 可以跑的字節(jié)碼,那 Java 里必然可以找到與之對(duì)應(yīng)的東西。
Android Studio 也提供了很方便的工具。選擇菜單 Tools -> Kotlin -> Show Kotlin Bytecode 即可看到 Kotlin 編譯成的字節(jié)碼,點(diǎn)擊窗口上方的 “Decompile” 即可看到這份字節(jié)碼對(duì)應(yīng)的 Java 代碼。—— 這個(gè)工具特別重要,假如一段 Kotlin 代碼讓你看得云里霧里,看一下它對(duì)應(yīng)的 Java 代碼你就能知道它的含義。
當(dāng)然這里僅僅是說(shuō)上手或入門(僅入門的話可以忽略諸如協(xié)程等高級(jí)特性),真正熟練應(yīng)用乃至完全掌握肯定需要一定時(shí)間。
二、針對(duì) NPE 的強(qiáng)規(guī)則
有些文章說(shuō) Kotlin 幫開(kāi)發(fā)者解決了 NPE(NullPointerException),這個(gè)說(shuō)法是不對(duì)的。 在我看來(lái),Kotlin 沒(méi)有幫開(kāi)發(fā)者解決了 NPE (Kotlin: 臣妾真的做不到啊),而是通過(guò)在語(yǔ)言層面增加各種強(qiáng)規(guī)則,強(qiáng)制開(kāi)發(fā)者去自己處理可能的空指針問(wèn)題,達(dá)到盡量減少(只能減少而無(wú)法完全避免)出現(xiàn) NPE 的目的。
那么 Kotlin 具體是怎么做的呢?別著急,我們可以先回顧一下在 Java 中我們是怎么處理空指針問(wèn)題的。
Java 中對(duì)于空指針的處理總體來(lái)說(shuō)可以分為“防御式編程”和“契約式編程”兩種方案。
“防御式編程”大家應(yīng)該不陌生,核心思想是不信任任何“外部”輸入 —— 不管是真實(shí)的用戶輸入還是其他模塊傳入的實(shí)參,具體點(diǎn)就是 各種判空 。創(chuàng)建一個(gè)方法需要判空,創(chuàng)建一個(gè)邏輯塊需要判空,甚至自己的代碼內(nèi)部也需要判空(防止對(duì)象的回收之類的)。示例如下:
public void showToast(Activity activity) { if (activity == null) { return; } ......}另一種是“契約式編程”,各個(gè)模塊之間約定好一種規(guī)則,大家按照規(guī)則來(lái)辦事,出了問(wèn)題找沒(méi)有遵守規(guī)則的人負(fù)責(zé),這樣可以避免大量的判空邏輯。Android 提供了相關(guān)的注解以及最基礎(chǔ)的檢查來(lái)協(xié)助開(kāi)發(fā)者,示例如下:
public void showToast(@NonNull Activity activity) { ......}在示例中我們給 Activity 增加了 @NonNull 的注解,就是向所有調(diào)用這個(gè)方法的人聲明了一個(gè)約定,調(diào)用方應(yīng)該保證傳入的 activity 非空。當(dāng)然聰明的你應(yīng)該知道,這是一個(gè)很弱的限制,調(diào)用方?jīng)]注意或者不理會(huì)這個(gè)注解的話,程序就依然還有 NPE 導(dǎo)致的 crash 的風(fēng)險(xiǎn)。
回過(guò)頭來(lái), 對(duì)于 Kotlin,我覺(jué)得就是一種把契約式編程和防御式編程相結(jié)合且提升到語(yǔ)言層面的處理方式。 (聽(tīng)起來(lái)似乎比 Java 中各種判空或注解更麻煩?繼續(xù)看下去,你會(huì)發(fā)現(xiàn)的確是更麻煩……)
在 Kotlin 中,有以下幾方面約束:
在聲明階段,變量需要決定自己是否可為空,比如 var time: Long? 可接受 null,而 var time: Long 則不能接受 null。
在變量傳遞階段,必須保持“可空性”一致,比如形參聲明是不為空的,那么實(shí)參必須本身是非空或者轉(zhuǎn)為非空才能正常傳遞。示例如下:
fun main() { ...... // test(isOpen) 直接這樣調(diào)用,編譯不通過(guò) // 可以是在空檢查之內(nèi)傳遞,證明自己非空 isOpen?.apply { test(this) } // 也可以是強(qiáng)制轉(zhuǎn)成非空類型 test(isOpen!!) } private fun test(open: Boolean) { ...... }在使用階段,需要嚴(yán)格判空:
var time: Long? = 1000 //盡管你才賦值了非空的值,但在使用過(guò)程中,你無(wú)法這樣: //time.toInt() //必須判空 time?.toInt()
總的來(lái)說(shuō) Kotlin 為了解決 NPE 做了大量語(yǔ)言層級(jí)的強(qiáng)限制,的確可以做到減少 NPE 的發(fā)生。但這種既“契約式”(判空)又“防御式”(聲明空與非空)的方案會(huì)讓開(kāi)發(fā)者做更多的工作,會(huì)更“麻煩”一點(diǎn)。
當(dāng)然,Kotlin 為了減少麻煩,用 “?” 簡(jiǎn)化了判空邏輯 —— “?” 的實(shí)質(zhì)還是判空,我們可以通過(guò)工具查看 time?.toInt() 的 Java 等價(jià)代碼是:
if (time != null) { int var10000 = (int)time;}這種簡(jiǎn)化在數(shù)據(jù)層級(jí)很深需要寫大量判空語(yǔ)句時(shí)會(huì)特別方便,這也是為什么 雖然邏輯上 Kotlin 讓開(kāi)發(fā)者做了更多工作,但寫代碼過(guò)程中卻并沒(méi)有感覺(jué)到更麻煩。
三、強(qiáng)規(guī)則之下的 NPE 問(wèn)題
在 Kotlin 這么嚴(yán)密的防御之下,NPE 問(wèn)題是否已經(jīng)被終結(jié)了呢?答案當(dāng)然是否定的。在實(shí)踐過(guò)程中我們發(fā)現(xiàn)主要有以下幾種容易導(dǎo)致 NPE 的場(chǎng)景:
1. data class(含義對(duì)應(yīng) Java 中的 model)聲明了非空
例如從后端拿 json 數(shù)據(jù)的場(chǎng)景,后端的哪個(gè)字段可能會(huì)傳空是客戶端無(wú)法控制的,這種情況下我們的預(yù)期 必須是 每個(gè)字段都可能為空,這樣轉(zhuǎn)成 json object 時(shí)才不會(huì)有問(wèn)題:
data class User( var id: Long?, var gender: Long?, var avatar: String?)
假如有一個(gè)字段忘了加上”?”,后端沒(méi)傳該值就會(huì)拋出空指針異常。
2. 過(guò)分依賴 Kotlin 的空值檢查
private lateinit var mUser: User...private fun initView() { mUser = intent.getParcelableExtra<User>("key_user")}在 Kotlin 的體系中久了會(huì)過(guò)分依賴于 Android Studio 的空值檢查,在代碼提示中 Intent 的 getParcelableExtra 方法返回的是非空,因此這里你直接用方法結(jié)果賦值不會(huì)有任何警告。但點(diǎn)擊進(jìn) getParcelableExtra 方法內(nèi)部你會(huì)發(fā)現(xiàn)它的實(shí)現(xiàn)是這樣的:
public <T extends Parcelable> T getParcelableExtra(String name) { return mExtras == null ? null : mExtras.<T>getParcelable(name); }內(nèi)部的其他代碼不展開(kāi)了,總之它是可能會(huì)返回 null 的,直接賦值顯然會(huì)有問(wèn)題。
我理解這是 Kotlin 編譯工具對(duì) Java 代碼檢查的不足之處, 它無(wú)法準(zhǔn)確判斷 Java 方法是否會(huì)返回空就選擇無(wú)條件信任,即便方法本身可能還聲明了 @Nullable 。
3. 變量或形參聲明為非空
這點(diǎn)與第一、第二點(diǎn)都很類似,主要是使用過(guò)程中一定要進(jìn)一步思考傳遞過(guò)來(lái)的值是否真的非空。
有人可能會(huì)說(shuō),那我全部都聲明為可空類型不就得了么 —— 這樣做會(huì)讓你在使用該變量的所有地方都需要判空,Kotlin 本身的便利性就蕩然無(wú)存了。
我的觀點(diǎn)是不要因噎廢食,使用時(shí)多注意點(diǎn)就可以避免大部分問(wèn)題。
4. !! 強(qiáng)行轉(zhuǎn)為非空
當(dāng)將可空類型賦值給非空類型時(shí),需要有對(duì)空類型的判斷,確保非空才能賦值(Kotlin 的約束)。
我們使用 !! 可以很方便得將“可空”轉(zhuǎn)為“非空”, 但可空變量值為 null,則會(huì) crash 。
因此使用上建議在確保非空時(shí)才用 !! :
param!!
否則還是盡量放在判空代碼塊里:
param?.let { doSomething(it) }四、實(shí)踐中碰到的問(wèn)題
從 Java 的空處理轉(zhuǎn)到 Kotlin 的空處理,我們可能會(huì)下意識(shí)去尋找對(duì)標(biāo) Java 的判空寫法:
if (n != null) { //非空如何 } else { //為空又如何}在 Kotlin 中類似的寫法的確有,那就是結(jié)合高階函數(shù) let、apply、run …… 來(lái)處理判空,比如上述 Java 代碼就可以寫成:
n?.let { //非空如何} ?: let { //為空又如何}但這里有幾個(gè)小坑。
1. 兩個(gè)代碼塊不是互斥關(guān)系
假如是 Java 的寫法,那么不管 n 的值怎樣,兩個(gè)代碼塊都是互斥的,也就是“非黑即白”。但 Kotlin 的這種寫法不是(不確定這種寫法是否是最佳實(shí)踐,假如有更好的方案可以留言指出)。
?: 這個(gè)操作符可以理解為 if (a != null) a else b ,也就是它之前的值非空返回之前的值,否則返回之后的值。
而上面代碼中這些高階函數(shù)都是有返回值的,詳見(jiàn)下表:
| 函數(shù) | 返回值 |
|---|---|
| let | 返回指定 return 或函數(shù)里最后一行 |
| apply | 返回該對(duì)象本身 |
| run | 返回指定 return 或函數(shù)里最后一行 |
| with | 返回指定 return 或函數(shù)里最后一行 |
| also | 返回該對(duì)象本身 |
| takeIf | 條件成立返回對(duì)象本身,不成立返回 null |
| takeUnless | 條件成立返回 null,不成立返回該對(duì)象本身 |
假如用的是 let, 注意看它的返回值是“指定 return 或函數(shù)里最后一行”,那么碰到以下情況:
val n = 1var a = 0n?.let { a++ ... null //最后一行為 null} ?: let { a++}你會(huì)很神奇地發(fā)現(xiàn) a 的值是 2,也就是 既執(zhí)行了前一個(gè)代碼塊,也執(zhí)行了后一個(gè)代碼塊 。
上面這種寫法你可能不以為然,因?yàn)楹苊黠@地提醒了諸位需要注意最后一行,但假如是之前沒(méi)注意這個(gè)細(xì)節(jié)或者是下面這種寫法呢?
n?.let { ... anMap.put(key, value) // anMap 是一個(gè) HashMap} ?: let { ...}應(yīng)該很少人會(huì)注意到 Map 的 put 方法是有返回值的,且可能會(huì)返回 null。那么這種情況下很容易踩坑。
2. 兩個(gè)代碼塊的對(duì)象不同
以 let 為例,在 let 代碼塊里可以用 it 指代該對(duì)象(其他高階函數(shù)可能用 this,類似的),那么我們?cè)趯懭缦麓a時(shí)可能會(huì)順手這樣寫:
activity { n?.let { it.hashCode() // it 為 n } ?: let { it.hashCode() // it 為 activity } }結(jié)果自然會(huì)發(fā)現(xiàn)值不一樣。前一個(gè)代碼塊 it 指代的是 n,而后一個(gè)代碼塊里 it 指代的是整個(gè)代碼塊指向的 this。
原因是 ?: 與 let 之間是沒(méi)有 . 的,也就是說(shuō) 后一個(gè)代碼塊調(diào)用 let 的對(duì)象并不是被判空的對(duì)象,而是 this 。(不過(guò)這種場(chǎng)景會(huì)出錯(cuò)的概率不大,因?yàn)樵诤笠粋€(gè)代碼塊里很多對(duì)象 n 的方法用不了,就會(huì)注意到問(wèn)題了)
后記
總的來(lái)說(shuō)切換到 Kotlin 還是比預(yù)期順利和舒服,寫慣了 Kotlin 后再回去寫 Java 反倒有點(diǎn)不習(xí)慣。今天先寫這點(diǎn),后面有其他需要總結(jié)的再分享。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持VeVb武林網(wǎng)。
新聞熱點(diǎn)
疑難解答
圖片精選