在 Kotlin 中使用可空性

1. 在你开始之前

本代码实验室将教你关于可空性和 null 安全性的重要性。可空性是在许多编程语言中常见的一个概念。它指的是变量能够没有值的能力。在 Kotlin 中,可空性被有意地处理以实现 null 安全性。

先决条件

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

你将学到什么

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

你需要什么

  • 一个可以访问 Kotlin Playground 的网页浏览器

2. 使用可空变量

什么是 null

在第一单元中,你学习了当声明一个变量时,需要立即为其赋值。例如,当声明一个 favoriteActor 变量时,你可以立即为其分配一个 "Sandra Oh" 字符串值。

val favoriteActor = "Sandra Oh"

A box that represents a favoriteActor variable that's assigned a

但是,如果你没有最喜欢的演员呢?你可能希望为该变量分配一个 "Nobody""None" 值。这不是一个好的方法,因为你的程序会将 favoriteActor 变量解释为具有 "Nobody""None" 值,而不是根本没有值。在 Kotlin 中,你可以使用 null 来表示变量没有关联的值。

A box that represents afavoriteActor variable that's assigned a null value.

要在代码中使用 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
}

你将收到此错误消息

A warning message that says:

在 Kotlin 中,可空类型和不可空类型之间存在区别

  • 可空类型是可以保存 null 的变量。
  • 不可空类型是不能保存 null 的变量。

只有在你明确允许它保存 null 时,类型才是可空的。正如错误消息所说,String 数据类型是不可空类型,因此你不能将变量重新赋值为 null

A diagram that shows how to declare nullable type variables. It starts with a var keyword followed by the name of the variable block, a semi colon, the type of the variable, a question mark, the equal sign, and the value block.  The type block and the question mark is denoted with Nullable type text marking that having the type followed with question mark is what makes it a nullable type.

要在 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. 创建一个 number 变量,其类型为可空 Int 类型,然后将其赋值为 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() 函数中的所有代码,然后声明一个 favoriteActor 变量,其类型为 String 类型,并将其赋值为你最喜欢的演员的姓名
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)
}

你将收到此错误消息

An error message that says,

此错误是 *编译错误*。如之前的代码实验室所述,当 Kotlin 由于代码中的语法错误而无法编译代码时,就会发生编译错误。

Kotlin 有意地应用语法规则,以便它可以实现 null *安全性*,它指的是保证 *不会在可能为* null *的变量上意外调用成员*。这并不意味着变量不能为 null。这意味着如果访问了变量的成员,则该变量不能为 null

这至关重要,因为如果尝试访问一个为 null 的变量的成员 - 称为 null *引用* - 在应用程序运行期间,应用程序会崩溃,因为 null 变量不包含任何属性或方法。这种类型的崩溃被称为 *运行时错误*,其中错误发生在代码编译并运行之后。

由于 Kotlin 的 null 安全特性,此类运行时错误得到了避免,因为 Kotlin 编译器会强制对可空类型进行 null *检查*。Null *检查* 指的是在访问变量并将其视为不可空类型之前,检查该变量是否可能为 null 的过程。如果你希望将可空值用作其不可空类型,则需要显式执行 null 检查。你将在本代码实验室后面的 **使用** if/else **条件语句** 部分了解这一点。

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

接下来,你将学习处理可空类型的各种技术和运算符。

使用 ?. 安全调用运算符

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

A diagram that shows a nullable variable block followed by a question mark, a dot, and a method or property block. There are no spaces in between.

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

安全调用运算符 ?. 允许更安全地访问可空变量,因为 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

使用 !! 非空断言运算符

您还可以使用 !! 非空断言运算符来访问可空变量的方法或属性。

A diagram that shows a nullable variable block followed by two exclamation points, a single dot, and a method or property block. There are no spaces in between.

在可空变量之后,您需要添加 !! 非空断言运算符,后跟 . 运算符,然后是方法或属性,中间没有任何空格。

顾名思义,如果您使用 !! 非空断言,则表示您断言变量的值不为 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 错误

An error message that says,

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

使用 if/else 条件语句

您可以在 if/else 条件语句的 if 分支中执行 null *检查*。

A diagram that shows a nullable variable block followed by an exclamation point, an equal sign, and null.

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

if/else 语句

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

A diagram that describes an if/else statement with the if keyword followed by parentheses with a null check block inside them, a pair of curly braces with body 1 inside them, an else keyword, and another pair of curly braces with a body 2 block inside them. The else clause is encapsulated with a dotted red box, which is annotated as optional.

当与 if/else 语句结合使用时,空检查非常有用

  • nullableVariable != null 表达式的空检查用作 if 条件。
  • if 分支内的主体 1 假设变量不为 null。因此,在此主体中,您可以像访问非空变量一样自由地访问变量的方法或属性,而无需使用 ?. 安全调用运算符或 !! 非空断言运算符。
  • else 分支内的主体 2 假设变量为 null。因此,在此主体中,您可以添加在变量为 null 时应运行的语句。else 分支是可选的。您只需使用 if 条件来运行 null 检查,而无需在 null 检查失败时提供默认操作。

当有多行代码使用可空变量时,使用 if 条件进行 null 检查会更方便。相比之下,对于可空变量的单个引用,?. 安全调用运算符更方便。

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

A diagram that describes an if/else expression with the val keyword followed by a name block, a colon, and non-null type block, an equal symbol, the if keyword, parentheses with a condition inside them, a pair of curly braces with body 1 inside them, an else keyword with another pair of curly braces, and a body 2 block inside them.

要将 if/else 表达式分配给非空类型

  • nullableVariable != null null 检查用作 if 条件。
  • if 分支内的主体 1 假设变量不为 null。因此,在此主体中,您可以像访问非空变量一样访问变量的方法或属性,而无需使用 ?. 安全调用运算符或 !! 非空断言运算符。
  • 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() {
    var favoriteActor: String? = "Sandra Oh"

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

使用 . 运算符直接访问 favoriteActor 变量的 length 属性。

  1. else 分支的主体中,添加一个 0
fun main() {
   var 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() {
    var 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 运算符之后的表达式将执行。

A diagram that shows the val keyword followed by a name block, an equal sign, a nullable variable block, a question mark, a dot, a method or property block, a question mark, a colon, and a default value block.

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

  1. 删除 if/else 条件语句,然后将 lengthOfName 变量设置为可空 favoriteActor 变量,并使用 ?. 安全调用运算符来调用其 length 属性
fun main() {
   var 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() {
    var 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
  • 要访问可空变量的方法或属性,您需要使用?.安全调用运算符或!!非空断言运算符。
  • 您可以使用if/else语句和null检查来在不可空上下文中访问可空变量。
  • 您可以使用if/else表达式将可空变量转换为不可空类型。
  • 当可空变量为null时,您可以使用if/else表达式或?: Elvis运算符提供一个默认值。

了解更多