在 Kotlin 中使用可为 null 性

1. 准备工作

在本 Codelab 中,您将了解“可为 null 性”以及 null 安全的重要性。“可为 null 性”是许多编程语言中的常见概念,代表您可对变量不设置任何值。在 Kotlin 中,系统会刻意处理可为 null 性,以实现 null 安全。

前提条件

  • 了解 Kotlin 编程基础知识,包括变量以及 println()main() 函数
  • 熟悉 Kotlin 条件条件,包括 if/else 语句和布尔值表达式
  • 了解 Kotlin 类,包括如何访问变量中的方法和属性。

学习内容

  • 什么是 null
  • 可为 null 类型与不可为 null 类型之间的区别
  • 什么是 null 安全、其重要性如何以及 Kotlin 如何实现 null 安全。
  • 如何使用 ?. 安全调用运算符和 !! 非 null 断言运算符访问可为 null 变量的方法和属性。
  • 如何使用 if/else 条件执行 null 检查。
  • 如何使用 if/else 表达式将可为 null 的变量转换为不可为 null 类型。
  • 如何使用 if/else 表达式或 ?: Elvis 运算符,在可为 null 的变量为 null 时提供默认值。

所需条件

  • 一个能够访问 Kotlin 园地的网络浏览器

2. 使用可为 null 的变量

什么是 null

在第 1 单元中,我们已经了解到,在声明变量时,需要立即为其赋值。例如,在声明 favoriteActor 变量时,可以立即为其赋予 "Sandra Oh" 字符串值。

val favoriteActor = "Sandra Oh"

这个盒子表示被赋予“Sandra Oh”字符串值的 favoriteActor 变量。

但要是您没有喜爱的演员,该怎么办?您可能想为变量赋予 "Nobody""None" 值。这并不是一个好方法,因为程序会将 favoriteActor 变量解读为具有 "Nobody""None" 值,而不是根本没有值。在 Kotlin 中,我们可以使用 null 来表示变量没有任何关联的值。

这个盒子表示被赋予 null 值的 afavoriteActor 变量。

如需在代码中使用 null,请按以下步骤操作:

  1. Kotlin 园地中,将 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 会很有用。

了解不可为 null 的变量和可为 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
}

系统会显示以下错误消息:

内容为“Null cannot be a value of a non-null type String”的警告消息。

在 Kotlin 中,有可为 null 类型与不可为 null 类型之分:

  • 可为 null 类型是指可以存储 null 值的变量
  • 不可为 null 类型是指不能存储 null 值的变量

只有当您明确让某个变量可以存储 null 值时,该变量才属于可为 null 类型。正如错误消息所示,String 数据属于不可为 null 类型,因此您无法为该变量重新赋予 null

此图展示了如何声明可为 null 类型的变量。这种变量以 var 关键字开头,后面依次是变量块的名称、分号、变量的类型、问号、等号和值块。类型块和问号使用“Nullable type”文字标示,表示该类型后接问号即变成可为 null 类型。

如需在 Kotlin 中声明可为 null 的变量,您需要在相应类型的末尾添加 ? 运算符。例如,String? 类型可以存储字符串或 null,而 String 类型只能存储字符串。如需声明某个可为 null 的变量,您需要明确添加可为 null 类型。如果没有可为 null 类型,Kotlin 编译器会推断该变量属于不可为 null 类型。

  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 值。

试试看

现在,您可以使用可为 null String? 类型了。那么您可以使用 Int 值来初始化变量并为其重新赋予 null 吗?

写入可为 null 的 Int

  1. 移除 main() 函数中的所有代码:
fun main() {

}
  1. 创建一个 number 变量,其中含有可为 null 的 Int 类型,然后为该变量赋予 10 值:
fun main() {
    var number: Int? = 10
}
  1. 输出 number 变量,然后运行此程序:
fun main() {
    var number: Int? = 10
    println(number)
}

输出符合预期,如下所示:

10
  1. number 变量重新赋予 null,以确认该变量可为 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. 处理可为 null 的变量

在前面的课程中,我们已学习如何使用 . 运算符访问不可为 null 的变量的方法和属性。在本部分中,我们将了解如何运用这项技巧访问可为 null 的变量的方法和属性。

如需访问不可为 null 的 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 个字符(包括空格)。您喜爱的演员名称中的字符数可能有所不同。

访问可为 null 的变量的属性

假设您想将 favoriteActor 变量设置为可为 null,以便没有喜爱演员的用户可以为该变量赋予 null

如需访问可为 null 的 favoriteActor 变量的属性,请按以下步骤操作:

  • favoriteActor 变量类型更改为可为 null 类型,然后运行此程序:
fun main() {
    var favoriteActor: String? = "Sandra Oh"
    println(favoriteActor.length)
}

系统会显示以下错误消息:

内容为“Only safe or non-null asserted calls are allowed on a nullable receiver of type String?”的错误消息。

此错误属于编译错误。如上一个 Codelab 中所述,如果由于代码中的语法错误而导致 Kotlin 无法编译代码,就会发生编译错误。

Kotlin 会刻意应用语法规则,以实现 null 安全,即保证不会意外调用 null 变量。但这并不表示变量不能为 null;而是表示,在访问某个变量的成员时,则该变量不能为 null

这一点至关重要,因为如果在应用运行期间尝试访问 null 变量的成员(称为 null 引用),应用会因 null 变量不含任何属性或方法而崩溃。此类崩溃称为“运行时错误”,即在代码完成编译和运行后发生的错误。

由于 Kotlin 具有 null 安全特性,因此 Kotlin 编译器会对可为 null 类型强制执行 null 检查,以免发生此类运行时错误。“Null 检查”是指在访问变量并将其视为不可为 null 类型之前,检查该变量是否可为 null 的过程。如果您想将可为 null 的值用作不可为 null 类型,则需要明确执行 null 检查。如需了解详情,请参阅本 Codelab 后面的使用 if/else 条件部分。

在此示例中,系统不允许直接引用 favoriteActor 变量的 length 属性,因为该变量有可能是 null,因此代码在编译时失败。

接下来,您将学习用来处理可为 null 类型的各种技巧和运算符。

使用 ?. 安全调用运算符

您可以使用 ?. 安全调用运算符访问可为 null 变量的方法或属性。

此图展示了可为 null 的变量块,后面依次跟问号、点以及方法或属性块,各项之间没有空格。

如需使用 ?. 安全调用运算符访问方法或属性,请在变量名称后面添加 ? 符号,并使用 . 表示法访问方法或属性。

?. 安全调用运算符可让您更安全地访问可为 null 的变量,因为 Kotlin 编译器会阻止变量成员为访问 null 引用而进行的任何尝试,并针对访问的成员返回 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 断言运算符来访问可为 null 的变量的方法或属性。

此图展示了可为 null 的变量块,后面依次跟两个感叹号、一个点以及方法或属性块,各项之间没有空格。

您需要在可为 null 的变量后面添加 !! 非 null 断言运算符,之后再跟 . 运算符,最后添加不含任何空格的方法或属性。

顾名思义,如果您使用 !! 非 null 断言运算符,即表示您断言变量的值不是 null,无论变量是否为该值都是如此。

?. 安全调用运算符不同,当可为 null 的变量确实为 null 时,使用 !! 非 null 断言运算符可能会导致系统抛出 NullPointerException 错误。因此,只有在变量始终为不可为 null 或设置了适当的异常处理时,才应使用该断言运算符。如果异常未得到处理,便会导致运行时错误。您将在本课程后面的单元中了解异常处理。

如需使用 !! 非 null 断言运算符访问 favoriteActor 变量的属性,请按以下步骤操作:

  1. favoriteActor 变量重新赋予喜爱演员的名称,然后在 println() 语句中将 ?. 安全调用运算符替换为 !! 非 null 断言运算符:
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 错误,内容如下:

内容为“Exception in thread "main" java.lang.NullPointerException”的错误消息。

此 Kotlin 错误显示您的程序在执行期间崩溃。因此,除非您确定变量不为 null,否则不建议使用 !! 非 null 断言运算符。

使用 if/else 条件

您可以在 if/else 条件中使用 if 分支来执行 null 检查

此图展示了可为 null 的变量块,后面依次跟感叹号、等号和 null。

如需执行 null 检查,您可以使用 != 比较运算符检查可为 null 的变量是否不等于 null

if/else 语句

if/else 语句可以与 null 检查一起使用,如下所示:

此图描述了 if/else 语句,其 if 关键字后面依次跟一对内含“null check”块的圆括号、一对内含“body 1”的大括号、一个 else 关键字,以及另一对内含“body 2”块的大括号。else 子句以红色虚线框封装,并带有“optional”注解。

将 null 检查与 if/else 语句结合使用具有以下优点:

  • nullableVariable != null 表达式的 null 检查会被用作 if 条件。
  • if 分支中的“body 1”会假定变量不可为 null。因此,在这个主体中,您可以随意访问变量的方法或属性,就好像变量是不可为 null 的变量一般,而不必使用 ?. 安全调用运算符或 !! 非 null 断言运算符。
  • else 分支中的“body 2”会假定变量为 null。因此,在这个主体中,您可以添加应在变量为 null 时运行的语句。else 分支是可选的。当 null 检查失败时,您只能使用 if 条件来运行 null 检查,而不执行默认操作。

如果有多行代码使用可为 null 的变量,那么将 null 检查与 if 条件搭配使用会更方便。相比之下,?. 安全调用运算符更适用于对可为 null 变量的单次引用。

如需编写一个 if/else 语句来对 favoriteActor 变量执行 null 检查,请按以下步骤操作:

  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 表达式结合使用,以将可为 null 的变量转换为不可为 null 的变量。

此图描述了 if/else 表达式,其 val 关键字后面依次跟一个“name”块、一个冒号、一个“non-null type”块、一个等号、if 关键字、一对内含条件的圆括号、一对内含“body 1”的大括号、else 关键字,以及另一对内含“body 2”块的大括号。

如需为 if/else 表达式赋予不可为 null 类型,请按以下步骤操作:

  • nullableVariable != null null 检查用作 if 条件。
  • if 分支中的“body 1”会假定变量不可为 null。因此,在这个主体中,您可以访问变量的方法或属性,就好像变量是不可为 null 的变量一般,而不必使用 ?. 安全调用运算符或 !! 非 null 断言运算符。
  • else 分支中的“body 2”会假定变量为 null。因此,在这个主体中,您可以添加应在变量为 null 时运行的语句。
  • 在主体 1 和 2 的最后一行中,您需要使用会生成不可为 null 类型的表达式或值,以便在 null 检查通过或失败时,将该表达式或值赋给不可为 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 关键字,后面依次跟名称块、等号、可为 null 的变量块、问号、点、方法或属性块、问号、冒号以及默认值块。

如需修改之前的程序以使用 ?: Elvis 运算符,请按以下步骤操作:

  1. 移除 if/else 条件,然后将 lengthOfName 变量设置为可为 null 的 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 属性之后,添加后跟 0 值的 ?: Elvis 运算符,然后运行此程序:
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 的变量。
  • 可将 null 赋给可为 null 的变量。
  • 若要访问可为 null 的变量的方法或属性,您需要使用 ?. 安全调用运算符或 !! 非 null 断言运算符。
  • 您可以将 if/else 语句与 null 检查搭配使用,以在不可为 null 的上下文中访问可为 null 的变量。
  • 您可以使用 if/else 表达式将可为 null 的变量转换为不可为 null 类型。
  • 您可以使用 if/else 表达式或 ?: Elvis 运算符,在可为 null 的变量为 null 时,提供默认值。

了解更多内容