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