什麼是 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)
//15val 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")
}
//15val 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 不強制要求使用呢?而是因為下列幾點:
- 開發者有時候不清楚例外要怎麼處理,但是因為系統要求一定要用 try…catch 包起來,但 catch 裡面是空的,所以當例外發生時,程式雖然不會崩潰,但是也出現了隱性的問題,就是該有動作的時候沒有動作。
- 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安全及異常
例外處理壞味道:將例外當作控制流程:http://teddy-chen-tw.blogspot.com/2017/04/blog-post_21.html