iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
Mobile Development

新手向Android&Kotlin學習紀錄30天系列 第 24

第24天 Kotlin小學堂(13) : null安全與異常

  • 分享至 

  • xImage
  •  

今天要來討論的是之前稍微提到過的null,null是一個特殊值,用來表示變數的值不存在。包含Java在內的許多語言,null常常導致程式崩潰。
為什麼程式會這麼常因為這一個特殊值null引起崩潰呢?第一行代碼的作者郭霖提到:「我認為主要是因為空指針是一種不受編成語言檢查的運行時的異常,由程序員主動通過邏輯判斷來避免,但即使是最出色的程序員,也不可能將所有潛在的空指針異常全部考慮到。」
所以,在kotlin語言中,要求做特別的宣告,讓工程師有意識的使用null,這有助於避免null相關的程式崩潰。

可空性 Nullability

如前所述,Kotlin需要做特別的宣告才可以指派null值給元素。這種被宣告可以指派null的元素就可以說可為空,反之就是不可空。比如說,設置一變數hp來紀錄遊戲玩家的血量值,這個hp就不可能為空,hp的血量可以為零,但不能為空,因這樣不合邏輯,0 與 null 是不同的概念。

使用「?」表示可空

那如何做這個特別的可空性宣告呢?其實就在類型後加上一個問號「?」就ok囉

如:
下面String?類型的變數就可以保存字串或null值

var hp :Int = null //會出現紅色蚯蚓底線
var skill :String? = null //沒問題

會出現紅色蚯蚓底線的原因,是由於Kotlin是一門編譯型語言,程式碼會先編譯成機器語言指令,再由一個編譯器這個特殊程式執行。在編譯階段,編譯器會先檢查程式碼是否符合特定要求,確認無誤後再編譯成機器指令。

所以我們把null指派給非空的hp變數,Kotlin就會拒絕編譯,這類在編譯期捕獲的錯誤叫做編譯時期錯誤

雖然是錯誤但是這是工程師在編輯程式碼的當下馬上可以修正的錯誤,比執行後回報異常錯誤再來抓蟲除錯,這樣的錯誤成本是小多了。

null安全

現在我們要對一個可空的變數呼叫其他函數,一樣會發生編譯錯誤。
點一下readLine()會看到這個函數的標頭是紅框處這樣,可以看到他返回的是一個String?類型,表示有可能是字串,也可能是null。

像上圖這三行,執行main函數,點一下控制台區塊,出現輸入游標時,便可以打字輸入。

接著,我們想讓不論輸入字母大小寫都轉成全小寫的帳號,所以在後面呼叫lowercase()函數。
但是萬一返回是null的話,lowercase()無法對不存在的值做轉成小寫的動作,所以這時候編譯器不給過,出現紅色蚯蚓底線。

fun main() {
     println("登入者帳號 :")
     val username = readLine().lowercase()//出現紅色蚯蚓底線
     println("登入者 : $username")
    //硬要執行的話,控制台報錯:
    //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
}

來看看這種情況如何處理?

處理可空類型3個常見方式

  1. 安全呼叫?.運算子
  2. 使用!!.運算子
  3. 使用if判斷式

1.安全呼叫?.運算子

使用?.呼叫方法或函數,讓編譯器知道如果遇到null值就跳過方法或函數呼叫。
將val 改成var,方便測試username = null的情況:
不管輸入什麼 最後都會印出 username : null

fun main() {
    println("登入者帳號 :")
    var username = readLine()?.lowercase() //使用?.呼叫函數,錯誤消失
    username = null //測試完成後,可以使用ctrl+/註解掉。
    println("登入者 : $username") //r結果:username : null
}

安全呼叫運算子也可放在賦值運算子左側,安全呼叫鍊中任一個為null,就不會執行右側的呼叫函數,如下:

person?.department?.head = managersPool.getManager()

聯合使用let和?.運算子

  • 標準函數let :可以在任何類型上呼叫,主要作用是在指定的作用域內定義一個或多個變數。使用let的便利:
    • let函數中的it指的就是呼叫他的變數,可保證不是null。
    • let會隱式返回(看不見return)運算式結果,這樣一來就可以把結果設定給變數。
    fun main() {
        val listWithNulls: List<String?> = listOf("Kotlin", null)
        for (item in listWithNulls) {
        // ?.的意思是:item不為null時才會呼叫let,結果輸出 Kotlin 並忽略 null
            item?.let { println(it) } 
        }
    }
    

2.使用!!.運算子

!!.的官方名稱是非空斷言運算子,可用於可空類型上來呼叫函數,不過應該盡量避免使用。他等於跟編譯器說:「不要囉嗦!不用檢查,給我執行就對了!!如果是null值就給我拋出異常」。
如下例:

fun main() {
    var username:String? = null
    println("username : ${username!!.lowercase()}") 
    //強制執行字串轉小寫,會拋出
    //「Exception in thread "main" java.lang.NullPointerException」
}

但是若有些情況,非常確定變數不可能為null的話是可以使用,這個需要工程師自行拿捏。

3.使用if判斷式

最後可以使用if判斷式來決定不是null的時候才執行:

fun main() {
    println("登入者帳號 :")
    var username = readLine()?.lowercase()
 // username = null (測試用)
    if (username != null ) {
         println("登入者 : $username")
    } else {
        println("username不可為null")
    }
}

使用?:空合併運算子

因為很像貓王Elvis Presley,所以也被稱為 Elvis運算子。這運算子意思是?:左邊的值為null的話,才對右邊取值。

fun main() {
    println("登入者帳號 :")
    var username = readLine()
    username = null                 //測試null值用
    val user = username ?: "guest"  //若username是null,則讓 user = "guest"
    println("登入者 : $user")       //結果: 登入者 : guest
}

過濾集合中的null

使用filterNotNull()過濾掉集合中的null

fun main() {
   val nullableList: List<Int?> = listOf(null, 5, 2, null)
    val intList: List<Int> = nullableList.filterNotNull()
    println(intList) // [5, 2]
}

參考:
kotlin權威2.0
kotlin文檔Null safety
kotlin文檔Nullability in Java and Kotlin

異常

Kotlin 中所有異常類都是 Throwable 類的子孫類。每個異常都有消息、堆棧回溯信息以及可選的原因。

而有異常表示程式出了問題,需要處理,否則便會中止。若有異常沒能捕捉到並即時處理的異常叫做未捕獲異常(unhandled exception),然後造成程式中止執行(崩潰)。

在kotlin世界中所有異常都是未檢查(unchecked)異常,就是對於是否要以try/catch語句來包覆可能出錯的程式碼,編譯器並不強制要求。...因為已檢查異常的處理的用法常常出現和發明者想像的不一樣,比如雖然應編譯器要求捕捉已檢查異常,但又直接被忽略,程式仍然編譯通過。此現象叫做「吞食異常」,反而造成更難偵錯。...反而帶來更多問題:程式碼重複、難以理解的復原邏輯、吞食異常又丟掉錯誤訊息。
節錄自kotlin權威2.0

拋出異常

Kotlin允許主動示意有異常發生,這又叫做拋出異常,由throw表達式觸發。像這樣:

自訂異常

  1. 自訂訊息 , throw是一個表達式所以可以放在Elvis運算子後面,用來拋出異常,並可以自訂訊息 : "Name required"。
fun main() {
    val person = Person()
    person.lastname ?: checkLastname("Lastname required")
}

fun checkLastname(message: String): Nothing {
    throw IllegalArgumentException(message)
}

class Person(val lastname: String? = null) 

運行看看:果然出現報錯並帶著我們的自訂訊息。

2. 自訂異常類別,繼承既有的異常類別,承上例,將這個錯誤自訂名稱為LastnameRequiredException

class LastnameRequiredException(): IllegalArgumentException("Lastname required")

fun main() {
    val person = Person()
    //拋出異常的部分改寫成我們自訂的異常
    person.lastname ?: throw LastnameRequiredException() 
}

運行看看,錯誤訊息成功改成自訂的異常 LastnameRequiredException。

捕捉並處理異常

使用try來捕捉異常並做處理,try 是一個表達式,代表它可以返回一個結果,而這結果不是 try 區塊就是 catch 區塊中的最後一句表達式,finally區塊則不影響結果。

  • try區塊 : 嘗試使用變數、運算式等,如果沒有異常就執行try語句,略過catch區塊。
  • catch區塊 : 在這裡面定義若try區塊某處引發異常,該做什麼事,catch語句接受各種邏輯。catch接受各種特定類型的異常當作參數,這裡允許捕獲Exception類型的任何異常。可以有零到多個catch 區塊。
  • finally區塊 : finally 區塊可以省略。但是catch與finally區塊至少應該存在一個。
try{
    // 一些程式碼
}
catch (e: SomeException) {
    // 處理程序
}
finally {
    // 可選的 finally 區塊
}

如下,只是簡單在控制台印出異常,並沒有出現紅色的報錯訊息:

fun main() {
    val a :String?= null
    try {
        Person(a!!)
    }catch (e:Exception){
        println(e)
    }
}

先決條件函數

意料之外的值會導致出人意表的行為,為了方便驗證輸入或偵錯以避開問題,Kotlin提供了一些方便的函數:先決條件函數,透過函數我們可以拋出自訂訊息的異常。
可以利用先決條件函數定義先決條件,必須滿足才能執行目標程式碼

函數 描述
checkNotNull 如果參數值為null,則拋出IllegalStateException,否則返回非null值
require 如果參數值為false,則拋出IllegalStateException
requireNOtNull 如果參數值為null,則拋出IllegalStateException,否則返回非null值
error 如果參數值為null,則拋出IllegalStateException並輸出錯誤訊息,否則返回非null值
assert 如果參數值為fslse,則拋出AssertionError,加上斷言(assertion)編輯器標記

表格節錄自:kotlin權威2.0

require函數之前有在介紹class類別時在類別中的初始化區塊使用過,像這樣第一個參數明確要求,若結果是false就會丟出後面的異常訊息,設定簡單好用而且可讀性也很高。
checkNotNull也很好懂,第一參數若是null便會丟出第二個參數的自訂異常訊息報錯。

class Robot(
    val type: String,
    val color: String,
    val batteryLife: Int
) {
    //初始化區塊 : 檢查type屬性有無設定、batteryLife 不可小於等於0、沒問題就會印出字串
    init {
        checkNotNull(type, { "type屬性必須設定" })
        require(batteryLife > 0, { "電池可能已損壞無法使用" })
        println("Initialize a new Robot object : type :$type ,color : $color , battery life : $batteryLife hours")
    }

}

參考 kotlin權威2.0 、kotlin文檔

終於進入最後一周T^T,大家明天見~


上一篇
第23天 Kotlin小學堂(12) : 物件
下一篇
第25天 Kotlin小學堂(14) : Lateinit & Lazy properties
系列文
新手向Android&Kotlin學習紀錄30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言