小談Kotlin的空處理的使用

這篇文章主要介紹了小談Kotlin的空處理的使用,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

近來關於 Kotlin 的文章着實不少,Google 官方的支持讓越來越多的開發者開始關注 Kotlin。不久前加入的項目用的是 Kotlin 與 Java 混合開發的模式,紙上得來終覺淺,終於可以實踐一把新語言。 本文就來小談一下 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 還是比預期順利和舒服,寫慣了 Kotlin 後再回去寫 Java 反倒有點不習慣。今天先寫這點,後面有其他需要總結的再分享。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章