小談Kotlin的空處理的使用
近來(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ō)上手容易,但真要切換到另一門(mén)語(yǔ)言,難免還是會(huì)躊躇是否有這個(gè)必要。現(xiàn)在因?yàn)楣ぷ麝P(guān)系直接上手 Kotlin,感受是 真香(上手的確容易) 。
首先在代碼閱讀層面,對(duì)于有 Java 基礎(chǔ)的程序員來(lái)說(shuō)閱讀 Kotlin 代碼基本無(wú)障礙,除去一些操作符、一些順序上的變化,整體上可以直接閱讀。
其次在代碼編寫(xiě)層面,僅需要改變一些編碼習(xí)慣。主要是:語(yǔ)句不要寫(xiě)分號(hào)、變量需要用 var 或 val 聲明、類(lèi)型寫(xiě)在變量之后、實(shí)例化一個(gè)對(duì)象時(shí)不用 “new” …… 習(xí)慣層面的改變只需要多寫(xiě)代碼,自然而然就適應(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ō)上手或入門(mén)(僅入門(mén)的話可以忽略諸如協(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í)的用戶(hù)輸入還是其他模塊傳入的實(shí)參,具體點(diǎn)就是 各種判空 。創(chuàng)建一個(gè)方法需要判空,創(chuàng)建一個(gè)邏輯塊需要判空,甚至自己的代碼內(nèi)部也需要判空(防止對(duì)象的回收之類(lèi)的)。示例如下:
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)成非空類(lèi)型 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í)很深需要寫(xiě)大量判空語(yǔ)句時(shí)會(huì)特別方便,這也是為什么 雖然邏輯上 Kotlin 讓開(kāi)發(fā)者做了更多工作,但寫(xiě)代碼過(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ì)傳空是客戶(hù)端無(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ò)分依賴(lài) Kotlin 的空值檢查
private lateinit var mUser: User ... private fun initView() { mUser = intent.getParcelableExtra<User>("key_user") }
在 Kotlin 的體系中久了會(huì)過(guò)分依賴(lài)于 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)都很類(lèi)似,主要是使用過(guò)程中一定要進(jìn)一步思考傳遞過(guò)來(lái)的值是否真的非空。
有人可能會(huì)說(shuō),那我全部都聲明為可空類(lèi)型不就得了么 —— 這樣做會(huì)讓你在使用該變量的所有地方都需要判空,Kotlin 本身的便利性就蕩然無(wú)存了。
我的觀點(diǎn)是不要因噎廢食,使用時(shí)多注意點(diǎn)就可以避免大部分問(wèn)題。
4. !! 強(qiáng)行轉(zhuǎn)為非空
當(dāng)將可空類(lèi)型賦值給非空類(lèi)型時(shí),需要有對(duì)空類(lèi)型的判斷,確保非空才能賦值(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 的判空寫(xiě)法:
if (n != null) { //非空如何 } else { //為空又如何 }
在 Kotlin 中類(lèi)似的寫(xiě)法的確有,那就是結(jié)合高階函數(shù) let、apply、run …… 來(lái)處理判空,比如上述 Java 代碼就可以寫(xiě)成:
n?.let { //非空如何 } ?: let { //為空又如何 }
但這里有幾個(gè)小坑。
1. 兩個(gè)代碼塊不是互斥關(guān)系
假如是 Java 的寫(xiě)法,那么不管 n 的值怎樣,兩個(gè)代碼塊都是互斥的,也就是“非黑即白”。但 Kotlin 的這種寫(xiě)法不是(不確定這種寫(xiě)法是否是最佳實(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 = 1 var a = 0 n?.let { a++ ... null //最后一行為 null } ?: let { a++ }
你會(huì)很神奇地發(fā)現(xiàn) a 的值是 2,也就是 既執(zhí)行了前一個(gè)代碼塊,也執(zhí)行了后一個(gè)代碼塊 。
上面這種寫(xiě)法你可能不以為然,因?yàn)楹苊黠@地提醒了諸位需要注意最后一行,但假如是之前沒(méi)注意這個(gè)細(xì)節(jié)或者是下面這種寫(xiě)法呢?
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,類(lèi)似的),那么我們?cè)趯?xiě)如下代碼時(shí)可能會(huì)順手這樣寫(xiě):
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ù)期順利和舒服,寫(xiě)慣了 Kotlin 后再回去寫(xiě) Java 反倒有點(diǎn)不習(xí)慣。今天先寫(xiě)這點(diǎn),后面有其他需要總結(jié)的再分享。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android實(shí)現(xiàn)帶簽到贏積分功能的日歷
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)帶簽到贏積分功能的日歷,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android編程獲取網(wǎng)絡(luò)連接方式及判斷手機(jī)卡所屬運(yùn)營(yíng)商的方法
這篇文章主要介紹了Android編程獲取網(wǎng)絡(luò)連接方式及判斷手機(jī)卡所屬運(yùn)營(yíng)商的方法,涉及Android針對(duì)網(wǎng)絡(luò)的判斷及本機(jī)信息的獲取技巧,需要的朋友可以參考下2016-01-01Android WindowManger的層級(jí)分析詳解
這篇文章主要介紹了Android WindowManger的層級(jí)分析詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09Android開(kāi)發(fā):微信授權(quán)登錄與微信分享完全解析
本篇文章主要介紹了Android微信授權(quán)登錄與微信分享,具有一定的參考價(jià)值,有需要的可以了解一下。2016-11-11Android 中RecyclerView多種item布局的寫(xiě)法(頭布局+腳布局)
這篇文章主要介紹了Android 中RecyclerView多種item布局的寫(xiě)法(頭布局+腳布局)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01Android自定義view實(shí)現(xiàn)輸入框效果
這篇文章主要為大家詳細(xì)介紹了Android自定義view實(shí)現(xiàn)輸入框效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03Webview實(shí)現(xiàn)android簡(jiǎn)單的瀏覽器實(shí)例代碼
這篇文章主要介紹了Webview實(shí)現(xiàn)android簡(jiǎn)單的瀏覽器實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-02-02Android自定義view制作抽獎(jiǎng)轉(zhuǎn)盤(pán)
這篇文章主要為大家詳細(xì)介紹了Android自定義view制作抽獎(jiǎng)轉(zhuǎn)盤(pán),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12android使用surfaceview+MediaPlayer播放視頻
這篇文章主要為大家詳細(xì)介紹了android使用surfaceview+MediaPlayer播放視頻,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Kotlin?協(xié)程的取消機(jī)制詳細(xì)解讀
這篇文章主要為大家介紹了Kotlin?協(xié)程的取消機(jī)制詳細(xì)解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10