保障程式的安全!

工程師們的願望就是希望自己寫出來的程式碼Bug越少越好,我們的程式越不會出錯,就代表程式越“安全”。而正好!Kotlin是一個注重安全性的程式語言,nullable型態讓我們可以順利、安全的處理“null”而不會無緣無故就跑出NullPointerException。最後我們會讓大家了解怎麼樣處理錯誤資訊。

移除物件參考

一般來說當我們宣告了一個變數,並且給予初始值後,編譯器會自動判斷該變數的型態:

var name = "小黑"

看到上面的語句,我們可以知道name的型態應該會是“String”,並且因為name是可變的變數,所以我們也可以指定另一個不同的物件參考給他:

name = "小固"

那如果在某些時候這個變數不需要物件參考的時候,我們要怎麼把物件的參考移除呢?

讓工程師們又愛又恨的null

如果我們要移除變數的參考,我們可以使用null。

null是什麼

null我們稱之為空值,空值不好理解,我們可以想像一下,變數就像是一支遙控器,變數參考的物件就是一台冷氣機。我們可以透過變數,操控參考的物件,就像是我們可以使用遙控器操控已經配對好的冷氣機一樣。

如果變數是空值,代表這個遙控器沒有配對到任何冷氣機。但是我們卻拿著什麼都沒有的遙控器在教室裡面亂按,這個時候編譯器就會覺得你很奇怪,他沒辦法理解你想要做什麼,所以就會拋出NullPointerException。

把變數設定成null

我們了解null之後就可以來試試看把變數設定成null了

name = null

可是當我們這樣做的時候卻發現編譯器出現了紅字,錯誤訊息顯示:“Null can not be a value of a non-null type String”。這是怎麼一回事?原來在Kotlin裡面,可以設定成null的變數需要有特別的宣告方式:

nullable型態

當我們需要把變數設定成null的時候,我們需要特別告訴編譯器:「這個變數有可能會是null喔!要特別小心注意他,以免他出事」。就像一個班級裡的問題學生一樣被老師特別關照,所以我們會用“?”來表示這個被“特別關照”的變數:

var name: String? = "小黑"
name = null

看!這個時候我們就可以把null放進這個變數裡面了。那我們可以在什麼地方使用nullable型態呢?

定義變數跟屬性的時候

我們可以把任何變數宣告成nullable型態,這個我們上面的例子已經有示範到了。至於什麼是屬性呢?我們等到後面學到物件的時候就會一起介紹囉~

定義函數參數的時候

在定義函數的時候使用nullable型態的參數代表著這個參數可以傳入null,但是這並不代表我們可以不傳入對應的引數,傳入null引數跟預設引數是不一樣的兩個概念,千萬別搞混了~

定義函數回傳型態的時候

定義函數的回傳值也可以使用nullable型態,這意味著函數的回傳值“有可能”會是null

有沒有發現上面的狀況跟lambda函數可以使用的地方是一樣的啊?沒有錯!不管是lambda函數還是nullable型態,他們本質上都是在跟你說“這個東西應該要是什麼”,所以他們可以使用的地方自然會是一樣的。

如何使用nullable型態?

使用if判斷null值

現在我們宣告了一個nullable的變數,可以我們卻發現宣告出來的變數,沒辦法正常的使用,會出現紅字

那是因為編譯器強迫我們,必須確認好這個變數不是null的時候,我們才可以正常的使用它。所以我們可以先檢查,確定變數是不是真的有東西,再使用變數:

var name: String? = "小黑"
if (name != null) {
    name.reversed()
}

而且我們也可以寫出更複雜的判斷式,只要我們能確定使用到變數的時候它一定是有東西的:

var name: String? = "小黑"
if (name != null && name.length < 3) {
    name.reversed()
}

但是判斷式並不是一個好方法,一來判斷式太多會很難閱讀,二來他沒辦法幫我們避免掉所有的問題,例如這樣的狀況:

var name: String? = "小黑"

fun main() {
    if (name != null) {
        name.reversed()
    }
}

‘這個例子不是跟前面的差不多嗎?為什麼前面的沒有問題,這裡就出問題了?’

“因為這個例子的變數是宣告在main函數外面,所以並不是只有main函數可以使用他!我們可能在其他地方同時使用到這個函數。萬一在判斷完變數是不是null之後,到使用變數之前,這個變數在其他地方被設定成null了,我們的程式就會出問題了!這個我們稱之為競態條件(Race Condition),之後我們會更詳細的探討這個問題”

使用安全呼叫(?.)

對於nullable型態的呼叫我們最常使用安全呼叫運算子(?.),就像我們應該隨時警惕nullable物件一樣,安全呼叫運算子看起來就像是隨時隨地的對nullable型態的變數保持警惕的樣子,只要東西不是null他就會通過讓呼叫執行,如果檢查到東西是null,就會攔下來不讓他做事。所以前面的範例我們改成安全呼叫,這樣就不會有問題了!

var name: String? = "小黑"

fun main() {
    name?.reversed()
}

使用let的安全呼叫

有些事情並不是變數本身可以做的,有些事情是需要把變數當引數傳進方法裡面,可是如果方法不接受nullable型態的話要怎麼辦?我們可以看看下面的範例怎麼處理:

var name: String? = "小黑"

fun main() {
    name?.let { doSomething(it) }
}

fun doSomething(text: String) {
    // do something
}

使用“?.let”我們就可以在lambda函數裡面的參數中取用到變數的“非nullable型態”,至於如果變數是null的話,就不會進入到lambda函數裡面。

使用Elvis(貓王)運算子(?:)

如果我們需要使用nullable的值,然後在他是null的時候回傳一個預設值的時候,我們就可以使用Elvis運算子:

var name: String? = "小黑"

fun main() {
    val length = name?.length ?: -1
    println(length)
}

Elvis運算子會檢查左邊的值是不是null,如果不是會回傳值,如果是null就會回傳右邊的值。至於為什麼會被叫做Elvis(貓王)運算子,是因為他旋轉90度後長的有點像貓王(Elvis Aaron Presley)

非null斷言運算子(!!)

非null斷言運算子是這幾種方法裡面最直接暴力的手段,使用他就像是你直接跟編譯器說:「這個東西絕對不是null!你給我直接把他當成非null型態來看就對了!」基本上,我會建議除非你100%確定這個東西不會是null的時候才使用,絕對不要因為貪圖方便去使用它,不然我們就會失去Kotlin帶給我們的安全性了。而萬一我們使用了斷言運算子,結果卻是null的話,會拋出NullPointerException。

var name: String? = "小黑"

fun main() {
    val length = name?.length!!
    println(length)
}

例外

例外是程式在執行的時候跳出的警告,用來跟我們說“我出事了!出了什麼事,在哪裡出事”的機制。就像剛剛的範例如果我們把"小黑"改成null的話。我們會看到下面的執行結果:

這邊我們可以看到幾個關鍵的地方:

  • NullPointerException:跟我們表示發生了什麼錯誤,錯誤的種類繁多,後面我們會跟大家介紹一些比較基本、常遇到的錯誤

  • “at com.kuro.MainKt.main(Main.kt:7)”:表示錯誤的地方在哪裡,這一系列的“at......”構成的描述錯誤出在什麼地方的東西我們稱呼他為“stack trace”。很多時候他會不只一行,我們需要耐心的看過並且找到真正的問題點,並解決他

例外被拋出的時候該怎麼辦?我們可以......

  • 放著不管他

  • 捕捉例外並且處理他

我們已經知道放著不管會發生什麼事了,甚至在手機app裡面會發生更可怕的事情“閃退”。為了不要讓我們的程式或是app運作到一半就沒辦法繼續運作下去,我們需要知道怎麼捕捉例外並且處理他。

嘗試(try)危險,並捕捉(catch)他

首先我們先來看看一段危險的程式碼:

fun main() {
    print("請輸入數字:")
    val inputNumber = readln().toInt()
    println(inputNumber)
}

這段程式碼會捕捉我們輸入的文字,轉換成整數並且印出來。看去來好像沒有問題對吧!?可是問題就出在我們可以輸入不是整數的文字,讓他爆炸(磅嘣!)像這樣一段危險的程式碼,我們就會使用try/catch包起來處理:

fun main() {
    try {
        print("請輸入數字:")
        val inputNumber = readln().toInt()
        println(inputNumber)
    } catch (e: NumberFormatException) {
        println("你輸入的不是整數!!")
    }
}

只要是在try裡面拋出的錯誤,我們就會把他接起來,並且留到catch裡面處理。catch後面的“( )”代表著這個catch區塊會處理什麼類型的錯誤(因為有些時候程式可能會拋出不只一種錯誤),如果我們需要處理多種錯誤,可以在後面增加更多catch區塊。

最後(finally)我們一定要做的事

如果我們有一段程式碼,是在不管try成功還是try失敗,我們都必須要執行的。那我們就可以把他寫在finally裡面

fun main() {
    try {
        print("請輸入數字:")
        val inputNumber = readln().toInt()
        println(inputNumber)
    } catch (e: NumberFormatException) {
        println("你輸入的不是整數!!")
    } finally {
        println("危機解除!")
    }
}

Last updated