Kotlin 線上讀書會 筆記 (三) Null Safety and Exception

Evan Chen
Evan Android Note
Published in
10 min readApr 6, 2020

這篇的範圍是第六章 Null Safety And Exception

Null Safety

在看Null Safety之前,我們先來看一下為什麼要有Null。

例如我們想要記錄一個學生的考試成績,而這個欄位自然會使用Int的型別來記錄考試的分數,但如果是缺考的話,我們要怎麼在這個Int的型別來記錄缺考。

//記錄考試成績
var score:Int

如果用0來記錄缺考是不適合的,因為考0分缺考是不一樣的,這時候我們要用null來表達缺考是較適合的。

var score:Int?
score = 0 //指的是0分,不能表達出缺考
score = null // 缺考

再舉另一個例子,寫App經常會需要呼叫API取得Json資料,例如你在做一個訂房的App,跟Web API取得飯店的資料。我們通常會把Json的結構對映到程式碼裡的資料模型。

{
"HotelName": "xx飯店",
"HotelEnglishName": "xx Hotel",
"HotelTraffic": {
"Hotel_Address": "台北市XX路…",
"Hotel_Lat": 24.222,
"Hotel_Lng": 125.222
}
}

可能API團隊就會跟你說Hotel_Traffic在某些情境是沒有的,這時候必須在HotelTraffic給null,因為如果用空白或是座標給奇怪的數字來表達沒有,都不太適合。

{
"HotelName": "xx飯店",
"HotelEnglishName": "xx Hotel",
"HotelTraffic": null
}

所以從現實生活中或從需求上來看,都是有「沒有」這件事的存在,也就有使用null的需求。

NullPointerException

null 的出現,但卻也造成了經常NullPointerException的錯誤。

String string = null;
string.length();

在Java 執行上面這段程式碼,是會出現NullPointerException的,因為string是null,卻呼叫length方法。更糟糕的是這個錯誤在執行階段才會知道,編譯器並不會事先告訴你有錯誤。

Null Safety

Kotlin在處理null可能發生的問題的作法就是,在宣告變數時先講清楚,到底可不可以是null。如果可以有null,編譯器就會提早檢查你是否有針對null做處理。

下例的程式碼,在宣告的類別後面加上?用來表示這個變數是可以null的。

//可以null
var name: String?
//不能null
var name2: String

宣告了一個可為null的變數後,如果直接呼叫name.length,編譯器就會警告錯誤。

var name: String? = null
println(name.length)

我們可按下Option + Enter ,IntelliJ會給予這些建議來修正。

選擇第1個Surround with null check,程式碼會加上if判斷是否null,這樣就確保了當name是null的時候,不會呼叫length方法。

var name: String? = null
if (name != null) {
println(name.length)
}

選擇第2個Replace with salfe(?.) call,則表示當name不是null的時候才呼叫length。

var name: String? = null
println(name?.length)

第3個方式,add non-null asserted(!!) call。 加上!!表示name一定不會是null,編譯器就不會再檢查。這種用法在使用上要特別小心,因為你必須真的確認不會是null,否則一執行還是會發生NullPointerException。

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

function的參數也是可以null的

function 裡的參數加上?後,表示該參數可以為null。當這樣寫時,你的function就必須處理可能有null的情境。所以如果沒有必要,就不要加上?,因為這樣就能確保function的參數傳進來的不可能是null。

fun setName(str: String?) {
//需處理傳進來str可能有null
}
fun setName(str: String) {
//不可能會有null進來了
}

如果傳的參數有null,但方法參數不允許,編譯器就會跳出錯誤。

fun main() {
var name: String?
setName(name)
}
fun setName(str: String) {
//不可能會有null進來了
}

按下Option + Enter,IntelliJ一樣會給建議的做法

第2種Wrap with ‘?.let{…}’ call。會在呼叫方法前加上.let,在name不為null時,才會執行大括號裡面的程式碼。

name?.let { setName(it) }

回傳型別

回傳型別也可以宣告為可null。

fun main() {
var str = getName()
}
fun getName() : String?{
return "A"
}

例如這個例子,getLocation的功能是傳入地址回傳座標Location?,回傳的Location是可以null的,這表示著傳入的座標如果找不到的話,就會回傳null。

fun main() {
getLocation("地址")
}
//找座標,用Location?來表示找不到
fun getLocation(address: String) : Location?{
if (找不到座標){
return null
}
return Location(1.0,1.0)
}
class Location (val lat:Double, val lng:Double)

Elvis operator

你可以用 ?: 來表示當變數為null時,該回傳的值

name?.length ?: 0

Primitive type

在Kotlin沒有Primitive type,所以Int、Boolean還是可以有null。

var a: Int? var: b:Boolean?

Exception

Exception 用來表達程式出了問題,你可以在程式碼有問題的地方加上throw Exception()

throw Exception("Hi There!")

什麼情況要丟出Exception呢?如下列程式碼,我要寫一個除法,傳入a、b兩個參數,而b不能為0。我們就可以在function裡先加上檢查,如果b=0就拋出一個異常。

fun division(a:Int, b:Int) :Int{
if (b == 0) {
throw Exception("b不可為0")
}
return a/b
}

catch exception

當錯誤發生時,可以使用try、catch把錯誤補抓下來。在catch裡面做當錯誤發生時應該做的處理。而finally指的是不管有沒有錯誤都要執行的區域。

try {
// some code
}
catch (e: SomeException) {
// handler
}
finally {
// optional finally block
}

不要忽略錯誤

不要在catch裡沒有任何的處理,直接忽略錯誤。

try {
log.append(message)
}
catch (IOException e) {
// Must be safe
}

Exception 之後

發生錯誤之後,記得在你的UI層級要去補抓錯誤,並告訴使用者現在應該怎麼辦。常見的做法是Alert一個錯誤的訊息。

自定義異常

你可以透過繼承RuntimeException來自定義一個Exception類別。自定義異常類別,可以更清楚拋出的異常是屬於哪一類的異常。下列就把呼叫WebAPI會發生的異常補抓下來,判斷Exception屬於APIFail還是APIForbidden,就可以做對映的處理。

fun main() {
try {
callAPI()
}catch (e:Exception){
when (e) {
is APIFail -> {
println("API Fail的處理")
}
is APIForbidden -> {
println("API Forbidden 的處理")
}
}
}
}
fun callAPI(): String {
throw APIFail()
}
class APIFail():RuntimeException()class APIForbidden():RuntimeException(

你也可以在自訂義的Exception類別裡,加上變數用來存放更多資訊。

fun getLevel(number: Int, type: String): String {
throw CustomError(1, "錯誤訊息")
}
class CustomError(var errorCode:Int, var errorMessage:String):RuntimeException(){}

Precondiction Exception

checkNotNull
如果參數是null,拋出IllegalStateException異常,否則回傳參數值

require
如果參數是false,拋出IllegalArgumentException異常

requireNotNull
如果參數是null,拋出IllegalStateException異常,否則回傳參數值

error
拋出異常訊息

assert
如果參數是false,拋出AssertionError異常

Elvis operator

你還可以使用?:的方式,在變數為null時拋出異常。

val s = name ?: throw IllegalArgumentException("Name required")

👊 給Android 初學者 的快速成長 線上課程

1️⃣ UI 進階實戰Material Design Component 讓你簡單做出效果超好的UI

2️⃣ 動畫入門到進階用動畫提升使用者體驗

3️⃣ 架構設計MVP、MVVM 讓你程式碼好維護

🆙 3堂組合包更划算 — Android 架構設計 + 動畫入門到進階 + UI 進階實戰

--

--