什麼是 null?

null 用來表示var 與 val 的變數不存在。是一個已經宣告但並未指向一個有效物件的指標。在許多程式語言中,null 會造成程式崩潰,因為讓不存在的東西做事是不可能的。

包含Java在內的許多程式語言,都包含一個最常見的陷阱,當使用一個指向null的物件時,就會得到null 參考例外(null reference exception),在Java中,稱作NullPointerException(NPE)。

Kotlin 的型別系統就是為了從程式碼中消除 null 的危險。

可空型別 (Nullable Types) 與不可空型別 (Non-null Types)

為了消除 NullPointerException(NPE),Kotlin 預設定義變數都是不可空的,如此一來,就不會因為設定空值,而發生出NPE。

但是,並非Kotlin就不存在null,在定義變數時,如果在變數的類別後面加上 ?,就表示此變數可為空。

fun main(){
var a: String = "abc"
a = null //compilation error
var b: String? = "abc"
b = null //ok
}
  • 當一個變數定義成不可空型別之後,就必須要考慮變數為空的情況。如下:
val b: String? = "nullable-string"
val length = if(b!=null){
b.length
}else{
-1
}
println(length)
  • 如果不考慮變數為空的情況,會發生什麼什麼事呢?

→ 編譯器會不讓我們編譯,並且提示我們只有 ?. 以及 !!. 可以使用。

接下來,將介紹 安全運算子 ?. 、非空斷言 (Not-null assertion operator)以及其他的運算子。

安全運算子 (Safe Calls operators)

?.

當變數設定為可空型別時,我們可以使用安全運算子 ?. 來呼叫變數。如下:

val a: String? = "nullable-string"
println(a?.length)
//15
val b: String? = null
println(b?.length)
//null

使用安全運算子時,當變數不為空時,我們就可以得到預期的數值;反之,如果是空的時候,不管呼叫什麼,都只會回傳 null 。

如此一來,就不會因為傳入的數值為空而發生 NPE。

標準函數 let

我們可以使用標準函數 let 來與安全運算子搭配使用,如下:

val a: String? = "nullable-string"
a?.let{
println(it)
}

使用 let ,在語義上就會比較清楚,如上例,閱讀程式碼時,我們就可以知道若 a 不為空 那麼就執行println(it);反之,若 a 為空,則不執行。

另外, let 函數中可以將 let 前方的變數帶入至 lambda 運算式中,所以就可以使用 it 。

鏈式呼叫 (Chain)

如果有一連串的含有不可空型別的呼叫,使用鏈式呼叫可以減少許多冗余的程式碼,如下:

bob?.department?.head?.name

在上方例子中, bob、department、head 都是可空型別,用鏈式呼叫時,只要有其中一個可空型別為空,那麼最後的結果就是 null 。

如果不使用鏈式呼叫,程式碼將變成:

var name:String? = nullif(bob!=null){
if(bob.department!=null){
if(bob.department.head!=null){
name = bob.department.head.name
}
}
}

空合併運算子 ?:

空合併運算子 ?: ,又名 Elvis 運算子。使用方法如下:

val a: String? = "nullable-string"val length = a?.length ?: -1prinln(length)//15

由上例,我們可以得知,在 ?: 的前面是擺放空運算子,右邊是擺放空值時的初始值。

等效於:

val length:Int = if(a != null) a.length else -1println(length)

另外,我們也可以結合 let?: ,如下例:

val b: String? = "nullable-string"
b?.let{println(it)} ?: println(-1)

非空斷言 (Not-null assertion operator)

上面介紹的安全運算子 ?. ,當變數為 null 時,只會回傳一個 null 的值,不會讓程式發生崩潰 ( Crash );但非空斷言 !!.,當變數為 null 時,程式會拋出 KotlinNullPointerException。

使用方法:在可空型別後面加上 !!.,如下例

val name: String? = "nullable-string"
val length = name!!.length
println(length)
//15

當變數為空時,使用非空斷言將會拋出 KotlinNullPointerException

val name: String? = null
val length = name!!.length
println(length)

例外處理

介紹例外處理之前,我們需要了解什麼是例外 ( Exception ):在程式語言中,例外通常會破壞原本程式正常的流程,例如,我們需要取得某字串的長度,但是因為這個字串為 null,所以長度無法取得,連帶後面使用長度的地方也會有問題,所以「無法取得長度」這件事就稱為例外。 前面介紹當使用非空斷言時,若變數為 null ,則會拋出 KotlinNullPointerException 。這也是另一種例外。

當例外發生時,我們用某些特殊的情況處理,稱之為例外處理 ( Exception Handling )。

一般在例外處理通常都是使用 try catch 來捕捉例外。如下例:

val a:String? = "nullable-string"try {
println(a!!.length)
} catch(e: KotlinNullPointerException){
println("a is null")
}
//15
val a:String? = nulltry {
println(a!!.length)
} catch(e: KotlinNullPointerException){
println("a is null")
}
//a is null

上例中,利用 try {} 將會發生例外的程式碼包起來,再將發生例外之後的程式碼放在 catch中。 如此,當 a 為 null 時,程式拋出 KotlinNullPointerException,但是我們使用 try catch 將程式碼包起來,且在 catch 中有指名要捕捉 KotlinNullPointerException 的例外,於是便會由我們預期的方式處理例外 ( println(“a is null”) )。

除了自己會拋出Exception 的函數,我們也可以自行拋出 Exception。

利用 throw 關鍵字,後面加上要拋出的 Exception 類,如此就能夠拋出例外。

throw Exception()

Kotlin 的例外處理

在大部分的程式語言中,使用 try…catch 處理例外已經是 common sense ,幾乎不管在什麼語言上,都能看到類似的語法,甚至連 Kotlin 都有包含。

但是,Kotlin 對於例外處理的方法跟其他語言不太一樣的地方是,他不強制要求使用 try catch 來處理例外, 而是你需要用 try catch 的時候才用。( 佛性例外處理 )

為什麼 Kotlin 在例外處理上會跟其他語言不一樣呢?例外就是很少發生的情況,為了很少發生的情況用 try catch 包起來不是不可以,那為什麼 Kotlin 不強制要求使用呢?而是因為下列幾點:

  1. 開發者有時候不清楚例外要怎麼處理,但是因為系統要求一定要用 try…catch 包起來,但 catch 裡面是空的,所以當例外發生時,程式雖然不會崩潰,但是也出現了隱性的問題,就是該有動作的時候沒有動作。
  2. try…catch 會創造隱性的控制流程,隱性的流程變多,就會難以維護。

Kotlin 這種不處理例外處理的方式,稱作為 Unchecked exception (不檢查例外)。

先決條件 (Precondition Function)

雖然 Kotlin 提供了安全運算子,希望程式不會因為變數為 null 時,而發生崩潰;但是在某些情況下, null 是不被允許的狀態,希望能夠拋出例外,除了非空斷言外,Kotlin 還提供了幾種先決條件函式 ( Precondition Function )

  • checkNotNull
  • requireNotNull
  • require
  • error
  • assert

checkNotNull / requireNotNull

— > 當變數為 null 時,將拋出 IllegalStateException;反之,回傳非空的值。

require

— > 當變數為 false 時,將拋出 IllegalArgumentException;反之,回傳非空的值。

error

— > 拋出 IllegalStateException,並且包含錯誤訊息。

assert

— > 變數為 false ,拋出 AssertError,參考斷言章節。

小結

在例外處理中,處理 NullPointerException (NPE) 佔了一大部分,Kotlin 利用不可空型別來讓變數不得為空,進而避免 NPE的情況。

雖然,Kotlin 預設是使用不可空型別來儲存變數,但還是有些時候必須要使用可空型別,在這種情況,就可以使用安全運算子來避免 NPE。

當然,如果非得使用可空型別,然後又必須要在 null 的時候拋出例外,可以使用非空斷言,或是先決條件函式。

參考

Kotlin 權威2.0:Android 專家養成術 — 第六章:null安全及異常

KotlinLang.org: Exception

KotlinLang.org: NullSafety

例外處理壞味道:將例外當作控制流程:http://teddy-chen-tw.blogspot.com/2017/04/blog-post_21.html

導讀投影片

導讀

--

--

Andy Lu
Andy Lu

Written by Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.

No responses yet