在 Kotlin 中使用是否可為空值

1. 事前準備

本程式碼研究室將教導您關於「是否可為空值」的概念以及 null 安全性的重要性。是否可為空值是許多程式設計語言中常見的概念,代表您可對變數不設定任何值。在 Kotlin 中,系統會刻意處理是否可為空值,以達成 null 安全性。

必要條件

  • Kotlin 程式設計的基本知識,包括變數與 println()main() 函式。
  • 熟悉 Kotlin 條件式,包含 if/else 陳述式與布林運算式
  • Kotlin 類別的相關知識,包含如何存取變數中的方法和屬性

課程內容

  • 什麼是 null
  • 可為空值和不可為空值這兩種類型的差異。
  • null 的安全性和重要性為何?以及 Kotlin 如何達成 null 安全性?
  • 如何使用 ?. 安全呼叫運算子和 !! 非空值斷言運算子,來存取可為空值變數的方法和屬性。
  • 如何使用 if/else 條件式執行「null 檢查」。
  • 如何使用 if/else 運算式將可為空值變數轉換成不可為空值類型。
  • 如何使用 if/else 運算式或 ?: Elvis 運算子,在可為空值變數為 null 時提供預設值。

軟硬體需求

  • 可使用 Kotlin Playground 的網路瀏覽器。

2. 使用可為空值變數

什麼是 null

在單元 1 中,您已瞭解在宣告變數時必須立即指派一個值。舉例來說,當您宣告 favoriteActor 變數時,就必須立即指派 "Sandra Oh" 字串值。

val favoriteActor = "Sandra Oh"

這個方塊代表獲派「"Sandra Oh"」字串值的 favoriteActor 變數。

不過,沒有喜愛的演員時該怎麼辦?您可能需要對變數指派 "Nobody""None" 值,但這並不是很好的做法,因為程式會將 favoriteActor 變數解讀為含有 "Nobody""None" 值,而不是完全沒有值。在 Kotlin 中,您可以使用 null 代表變數沒有任何相關聯的值。

這個方塊代表獲派空值的 afavoriteActor 變數。

如要在程式碼中使用 null,請按照下列步驟操作:

  1. Kotlin Playground 中,將 main() 函式主體內容替換成設為 nullfavoriteActor 變數:
fun main() {
    val favoriteActor = null
}
  1. 使用 println() 函式輸出 favoriteActor 變數的值,然後執行這個程式:
fun main() {
    val favoriteActor = null
    println(favoriteActor)
}

輸出結果如以下程式碼片段所示:

null

為變數重新指派 null

您先前已學會將以 var 關鍵字定義的變數重新指派給相同類型的不同值。舉例來說,您可為已使用某個名稱宣告的 name 變數重新指派另一個名稱,但前提是新名稱屬於 String 類型。

var favoriteActor: String = "Sandra Oh"
favoriteActor = "Meryl Streep"

有時候,您在宣告變數後可能會想將該變數指派給 null。舉例來說,在宣告喜愛的演員後,您可以決定完全不要公開自己喜愛的演員。在這類情況下,將 favoriteActor 變數指派給 null 是相當實用的做法。

瞭解不可為空值和可為空值的變數

如要將 favoriteActor 變數重新指派給 null,請按照下列步驟操作:

  1. val 關鍵字變更為 var 關鍵字,然後將 favoriteActor 變數指定為 String 類型,並指派給喜愛的演員名稱:
fun main() {
    var favoriteActor: String = "Sandra Oh"
    println(favoriteActor)
}
  1. 移除 println() 函式:
fun main() {
    var favoriteActor: String = "Sandra Oh"
}
  1. favoriteActor 變數重新指派給 null,然後執行這個程式:
fun main() {
    var favoriteActor: String = "Sandra Oh"
    favoriteActor = null
}

系統會顯示下列錯誤訊息:

警告訊息顯示「空值無法做為不可為空值類型字串的值」。

在 Kotlin 中,可分為可為空值和不可為非空值類型:

  • 可為空值類型是「可以」包含 null 的變數。
  • 不可為空值類型是「無法」包含 null 的變數。

當您明確在變數中放置 null 時,才是可為空值類型。如錯誤訊息所示,String 資料類型屬於不可為空值類型,因此您無法將變數重新指派給 null

這張圖表展示如何宣告可為空值類型變數。變數以 var 關鍵字為開頭,後面依序為變數區塊名稱、分號、變數類型、問號、等號和值區塊。類型區塊和問號以「可為空值類型」文字標示,這代表該類型後方加上問號後即變成可為空值類型。

如要在 Kotlin 中宣告可為空值變數,您必須在類型區塊結尾加上 ? 運算子。舉例來說,String? 類型可包含字串或 null,而 String 類型則只能包含字串。如要宣告可為空值變數,您必須明確加入可為空值類型。如果沒有可為空值類型,Kotlin 編譯器會推論該變數屬於不可為空值類型。

  1. favoriteActor 變數類型從 String 資料類型變更為 String? 資料類型:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    favoriteActor = null
}
  1. 在重新指派 null 前後輸出 favoriteActor 變數,然後執行這個程式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    println(favoriteActor)

    favoriteActor = null
    println(favoriteActor)
}

輸出結果如以下程式碼片段所示:

Sandra Oh
null

favoriteActor 變數原本含有字串,接著會轉換成 null

試用

您已瞭解如何使用可為空值的 String? 類型,那麼現在可以透過 Int 值將變數初始化並重新指派給 null 嗎?

寫入可為空值的 Int

  1. 移除 main() 函式中的所有程式碼:
fun main() {

}
  1. 建立可為空值 Int 類型的 number 變數,然後為該變數指派 10 值:
fun main() {
    var number: Int? = 10
}
  1. 輸出 number 變數,然後執行這個程式:
fun main() {
    var number: Int? = 10
    println(number)
}

輸出內容與預期結果相符,內容如下:

10
  1. number 變數重新指派給 null,以確認變數可為空值:
fun main() {
    var number: Int? = 10
    println(number)

    number = null
}
  1. 在程式的最後一行新增其他 println(number) 陳述式,然後執行程式:
fun main() {
    var number: Int? = 10
    println(number)

    number = null
    println(number)
}

輸出內容與預期結果相符,內容如下:

10
null

3. 處理可為空值變數

您先前已學習如何使用 . 運算子存取不可為空值變數的方法和屬性。本節將說明如何使用這個運算子存取可為空值變數的方法和屬性。

如要存取不可為空值的 favoriteActor 變數屬性,請按照下列步驟操作:

  1. 移除 main() 函式中的所有程式碼,然後宣告 String 類型的 favoriteActor 變數,並指派給喜愛的演員名稱:
fun main() {
    var favoriteActor: String = "Sandra Oh"
}
  1. 使用 length 屬性輸出 favoriteActor 變數值中的字元數,然後執行這個程式:
fun main() {
    var favoriteActor: String = "Sandra Oh"
    println(favoriteActor.length)
}

輸出內容與預期結果相符,內容如下:

9

favoriteActor 變數值中有「9」個字元,包含空格。您喜愛的演員名稱中的字元數可能會不同。

存取可為空值變數的屬性

假設您要讓 favoriteActor 變數成為可為空值的值,進而讓沒有喜愛演員的使用者能將變數指派為 null

如要存取可為空值的 favoriteActor 變數屬性,請按照下列步驟操作:

  • favoriteActor 變數類型變更為可為空值類型,然後執行這個程式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    println(favoriteActor.length)
}

系統會顯示下列錯誤訊息:

這則錯誤訊息顯示「只允許對可為空值的 String 類型接收器發出安全或非空值的斷言呼叫?」。

這類錯誤屬於「編譯錯誤」。如先前的程式碼研究室所述,當程式碼中有語法錯誤,導致 Kotlin 無法編譯程式碼時,就會發生編譯錯誤。

Kotlin 會刻意套用語法規則,以便達成 null「安全性」,意即保證「不會在意外情況下對 null 變數發出任何呼叫」。但這並不表示變數不能為 null,而是表示當您存取變數成員時,該變數不能是 null

這點非常重要,因為如果在應用程式執行期間嘗試存取 null 變數成員 (稱為「null 參照」),應用程式就會因 null 變數不含任何屬性或方法而停止運作。這類停止運作的狀況稱為「執行階段錯誤」,意即程式碼完成編譯及執行後發生的錯誤。

由於 Kotlin 具備 null 安全性,因此 Kotlin 編譯器會對可為空值類型強制執行「null 檢查」,以免這類執行階段錯誤發生。「Null 檢查」是指在存取變數並將其視為不可為空值類型之前,先檢查變數是否可為 null 的程序。如果您要將可為空值的值當做不可為空值類型使用,就必須明確執行 null 檢查。如要瞭解相關資訊,請參閱本程式碼研究室後續的「使用 if/else 條件式」一節。

在本範例中,系統不允許直接參照 favoriteActor 變數的 length 屬性 (原因為該變數很可能是 null),導致程式碼在編譯時間期間執行失敗。

接下來,您將瞭解用來處理可為空值類型的多種技巧和運算子。

使用 ?. 安全呼叫運算子

您可以使用 ?. 安全呼叫運算子存取可為空值變數的方法或屬性。

這張圖表顯示可為空值變數區塊,後面依序接有問號、點以及方法或屬性區塊。這幾個項目之間沒有空格。

如要使用 ?. 安全呼叫運算子存取方法或屬性,請在變數名稱後方加上 ? 符號,並使用 . 標記存取方法或屬性。

?. 安全呼叫運算子可讓您安全存取可為空值變數,因為 Kotlin 編譯器會阻止變數成員存取 null 參照,並對存取的成員傳回 null

如要安全存取可為空值的 favoriteActor 變數屬性,請按照下列步驟操作:

  1. println() 陳述式中,將 . 運算子替換成 ?. 安全呼叫運算子:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    println(favoriteActor?.length)
}
  1. 執行這個程式,並確認輸出內容符合預期:
9

您喜愛的演員名稱中的字元數可能會不同。

  1. favoriteActor 變數重新指派給 null,然後執行這個程式:
fun main() {
    var favoriteActor: String? = null
    println(favoriteActor?.length)
}

輸出內容如下:

null

請注意,即使嘗試存取 null 變數的 length 屬性,該程式仍不會當機。安全呼叫運算式只會傳回 null

使用 !! 非空值斷言運算子

您也可以使用 !! 非空值斷言運算子,來存取可為空值變數的方法或屬性。

這張圖表展示可為空值變數區塊,後面依序接有兩個驚嘆號、一個點以及方法或屬性區塊。這幾個項目之間沒有空格。

您必須在可為空值變數後方加上 !! 非空值斷言運算子,接著加上 . 運算子,最後是方法或屬性 (不含任何空格)。

顧名思義,如果您使用 !! 非空值斷言運算子,即表示您聲明變數的值並非 null,無論變數是否為該值都一樣。

如果可為空值變數確實是 null,使用 !! 非空值斷言運算子可能會導致系統擲回 NullPointerException 錯誤,這點與 ?. 安全呼叫運算子不同。因此,只有在變數一律為不可為空值,或已設定適當的例外狀況處理方式時,才需要使用該斷言運算子。如果例外狀況未妥善處理,就會造成執行階段錯誤。本課程的後續單元將說明例外狀況處理方式。

如要使用 !! 非空值斷言運算子存取 favoriteActor 變數的屬性,請按照下列步驟操作:

  1. favoriteActor 變數重新指派為您喜愛的演員名稱,然後在 println() 陳述式中將 ?. 安全呼叫運算子替換成 !! 非空值斷言運算子:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    println(favoriteActor!!.length)
}
  1. 執行這個程式,並確認輸出內容符合預期:
9

您喜愛的演員名稱中的字元數可能會不同。

  1. favoriteActor 變數重新指派給 null,然後執行這個程式:
fun main() {
    var favoriteActor: String? = null
    println(favoriteActor!!.length)
}

系統會顯示 NullPointerException 錯誤,內容如下:

這則錯誤訊息顯示「執行緒 "main" 中的例外狀況 java.lang.NullPointerException」。

這個 Kotlin 錯誤顯示程式在執行期間異常終止。因此,除非您確定變數不是 null,否則不建議使用 !! 非空值斷言運算子。

使用 if/else 條件式

您可以在 if/else 條件式中使用 if 分支版本來執行「null 檢查」

這張圖表展示可為空值變數區塊,後面依序接有驚嘆號、等號和空值。

如要執行 null 檢查,可以使用 != 比較運算子檢查可為空值變數不等於 null 的情形。

if/else 陳述式

if/else 陳述式可與 null 檢查搭配使用,如下所示:

這張圖表說明 if/else 陳述式的結構。開頭是 if 關鍵字,後面依序是加上括號空值檢查區塊、加上一組大括號的主體 1、else 關鍵字,然後是加上另一組大括號的主體 2 區塊。else 子句以紅色虛線方塊框住,並標註為選用項目。

空值檢查與 if/else 陳述式搭配使用時有以下優點:

  • nullableVariable != null 運算式的 null 檢查會做為 if 條件式使用。
  • if 分支版本內的主體 1 會假設變數不可為空值。因此,您可以將該變數視為是不可為空值變數一般,在這個主體中自由存取變數的方法或屬性,無需使用 ?. 安全呼叫運算子或 !! 非空值斷言運算子。
  • else 分支版本內的主體 2 會假設變數為 null。因此,當變數為 null 時,您就能在這個主體中新增應當執行的陳述式。else 分支版本為選用項目。當 null 檢查失敗時,您只能使用 if 條件式來執行 null 檢查,且不提供預設動作。

如果有多行程式碼使用可為空值變數,那麼將 null 檢查與 if 條件式搭配使用較為簡便。相反地,?. 安全呼叫運算子則適合用於可為空值變數的單一參照。

如要為 favoriteActor 變數編寫含有 null 檢查的 if/else 陳述式,請按照下列步驟操作:

  1. favoriteActor 變數再次指派給您喜愛的演員名稱,然後移除 println() 陳述式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

}
  1. 新增包含 favoriteActor != null 條件式的 if 分支版本:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    if (favoriteActor != null) {

    }
}
  1. if 分支版本的主體中,新增可接受 "The number of characters in your favorite actor's name is ${favoriteActor.length}" 字串的 println 陳述式,然後執行這個程式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    if (favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    }
}

輸出內容與預期結果相符。

The number of characters in your favorite actor's name is 9.

您喜愛的演員名稱中的字元數可能會不同。

請注意,由於您會在 null 檢查後存取 if 分支內的 length 方法,因此可直接使用 . 運算子存取名稱長度方法。同時,Kotlin 編譯器知道 favoriteActor 變數絕不會是 null,所以允許直接存取屬性。

  1. 選用:新增 else 分支版本,以處理演員姓名為 null 的情形:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    if (favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    } else {

    }
}
  1. else 分支版本的主體中,新增可接受 "You didn't input a name." 字串的 println 陳述式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    if (favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    } else {
      println("You didn't input a name.")
    }
}
  1. favoriteActor 變數指派給 null,然後執行這個程式:
fun main() {
    var favoriteActor: String? = null

    if(favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    } else {
      println("You didn't input a name.")
    }
}

輸出內容與預期結果相符,內容如下:

You didn't input a name.

if/else 運算式

您也可以將 null 檢查與 if/else 運算式結合,藉此將可為空值變數轉換成不可為空值變數。

這張圖表說明 if/else 運算式的結構。開頭是 val 關鍵字,後面依序接有名稱區塊、分號、非空值類型區塊、等號、if 關鍵字、加上括號的條件、加上一組大括號的主體 1、else 關鍵字以及加上另一組大括號的主體 2 區塊。

如何將 if/else 運算式指派給不可為空值類型:

  • nullableVariable != null null 檢查做為 if 條件式使用。
  • if 分支版本內的主體 1 會假設變數不可為空值。因此,您可以將該變數視為是不可為空值變數一般,在這個主體中存取變數的方法或屬性,無需使用 ?. 安全運算子或 !! 非空值斷言運算子。
  • else 分支版本內的主體 2 會假設變數為 null。因此,當變數為 null 時,您就能在這個主體中新增應當執行的陳述式。
  • 在主體 1 和主體 2 的最後一行中,您必須使用會產生不可為空值類型的運算式或值,這樣一來,就能在 null 檢查通過或失敗時指派給不可為空值變數。

如要使用 if/else 運算式重新編寫程式,讓程式只使用一個 println 陳述式,請按照下列步驟操作:

  1. favoriteActor 變數指派給您喜愛的演員名稱:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    if (favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    } else {
      println("You didn't input a name.")
    }
}
  1. 建立 lengthOfName 變數,然後指派給 if/else 運算式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    val lengthOfName = if(favoriteActor != null) {
      println("The number of characters in your favorite actor's name is ${favoriteActor.length}.")
    } else {
      println("You didn't input a name.")
    }
}
  1. ifelse 這兩個分支版本中移除 println() 陳述式:
fun main() {
    var favoriteActor: String? = "Sandra Oh"

    val lengthOfName = if(favoriteActor != null) {

    } else {

    }
}
  1. if 分支版本的主體中新增 favoriteActor.length 運算式:
fun main() {
    val favoriteActor: String? = "Sandra Oh"

    val lengthOfName = if(favoriteActor != null) {
      favoriteActor.length
    } else {

    }
}

favoriteActor 變數的 length 屬性可直接使用 . 運算子存取。

  1. else 分支版本的主體中新增 0 值:
fun main() {
    val favoriteActor: String? = "Sandra Oh"

    val lengthOfName = if(favoriteActor != null) {
      favoriteActor.length
    } else {
      0
    }
}

當名稱為 null 時,0 值會成為預設值。

  1. main() 函式的結尾加上可接受 "The number of characters in your favorite actor's name is $lengthOfName." 字串的 println 陳述式,然後執行這個程式:
fun main() {
    val favoriteActor: String? = "Sandra Oh"

    val lengthOfName = if(favoriteActor != null) {
      favoriteActor.length
    } else {
      0
    }

    println("The number of characters in your favorite actor's name is $lengthOfName.")
}

輸出內容與預期結果相符,內容如下:

The number of characters in your favorite actor's name is 9.

您使用的名稱字元數可能會不同。

使用 ?: Elvis 運算子

?: Elvis 運算子是指可與 ?. 安全呼叫運算子搭配使用的運算子。使用 ?: Elvis 運算子時,您可以在 ?. 安全呼叫運算子傳回 null 時加上預設值。這個做法與 if/else 運算式類似,但較為常用。

如果變數「不是」null,系統會執行 ?: Elvis 運算子之前的運算式。如果變數「是」null,則系統會執行 ?: Elvis 運算子之後的運算式。

這張圖表顯示 val 關鍵字,後面依序接有名稱區塊、等號、可為空值變數區塊、問號、點、方法或屬性區塊、問號、冒號和預設值區塊。

如要修改先前的程式以使用 ?: Elvis 運算子,請按照下列步驟操作:

  1. 移除 if/else 條件式,然後將 lengthOfName 變數設為可為空值的 favoriteActor 變數,並使用 ?. 安全呼叫運算子呼叫其 length 屬性:
fun main() {
    val favoriteActor: String? = "Sandra Oh"

    val lengthOfName = favoriteActor?.length

    println("The number of characters in your favorite actor's name is $lengthOfName.")
}
  1. length 屬性後方加上 ?: Elvis 運算子以及 0 值,然後執行這個程式:
fun main() {
    val favoriteActor: String? = "Sandra Oh"

    val lengthOfName = favoriteActor?.length ?: 0

    println("The number of characters in your favorite actor's name is $lengthOfName.")
}

輸出內容會與先前的輸出內容相同:

The number of characters in your favorite actor's name is 9.

4. 結語

恭喜!您已學會是否可為空值的概念,以及如何運用各種運算子進行管理。

摘要

  • 可將變數設為 null,代表該變數不含任何值。
  • 不可將 null 指派給不可為空值變數。
  • 可將 null 指派給可為空值變數。
  • 如要存取可為空值變數的方法或屬性,必須使用 ?. 安全呼叫運算子或 !! 非空值斷言運算子。
  • 可使用含有 null 檢查的 if/else 陳述式,在不可為空值項目中存取可為空值變數。
  • 可使用 if/else 運算式將可為空值變數轉換成不可為空值類型。
  • 當可為空值變數為 null 時,可使用 if/else 運算式或 ?: Elvis 運算子提供該變數的預設值。

瞭解詳情