在 Kotlin 中使用可空性

1. 开始之前

本 Codelab 教您了解可空性以及 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

在第 1 单元中,您学习到声明变量时,需要立即为其赋值。例如,当您声明一个 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() 函数主体中的内容替换为将 favoriteActor 变量设置为 null 的代码。
fun main() {
    val favoriteActor = null
}
  1. 使用 println() 函数打印 favoriteActor 变量的值,然后运行该程序。
fun main() {
    val favoriteActor = null
    println(favoriteActor)
}

输出类似于以下代码段:

null

使用 null 重新赋值变量

之前,您了解到可以使用 var 关键字定义的变量重新赋值为同一类型的不同值。例如,只要新名称是 String 类型,就可以将声明时带有一个名称的 name 变量重新赋值为另一个名称。

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. 创建一个可空 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 变量的值中有个字符,包括空格。您最喜欢的演员名称中的字符数可能不同。

访问可空变量的属性

假设您希望将 favoriteActor 变量设置为可空,以便没有最喜欢的演员的人可以将变量赋值为 null

要访问可空 favoriteActor 变量的属性,请执行以下步骤:

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

您会收到此错误消息:

An error message that says,

此错误是编译错误。如之前的 Codelab 所述,当 Kotlin 无法编译代码时(由于代码中的语法错误),就会发生编译错误。

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

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

由于 Kotlin 的 null 安全特性,可以防止此类运行时错误,因为 Kotlin 编译器会强制对可空类型进行 null 检查Null 检查指的是在访问变量并将其视为非可空类型之前,检查变量是否可能为 null 的过程。如果您希望将可空值用作其非可空类型,则需要明确执行 null 检查。您将在本 Codelab 后面的使用 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 错误。因此,只有在确定变量始终不为 null 或已设置适当的异常处理时才应使用它。未处理的异常会导致运行时错误。您将在本课程后续单元中学习异常处理。

要使用 !! 非空断言运算符访问 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 语句结合使用时,null 检查非常有用。

  • nullableVariable != null 表达式的 null 检查用作 if 条件。
  • if 分支内的 Body 1 假设变量不为 null。因此,在此主体中,您可以自由地访问变量的方法或属性,就像它是一个非可空变量一样,无需使用 ?. 安全调用运算符或 !! 非空断言运算符。
  • else 分支内的 Body 2 假设变量为 null。因此,在此主体中,您可以添加在变量为 null 时应运行的语句。else 分支是可选的。您可以使用 if 条件语句仅执行 null 检查,而无需在 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 分支的主体中,添加一个 println 语句,接受字符串 "The number of characters in your favorite actor's name is ${favoriteActor.length}.",然后运行该程序。
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.

您最喜欢的演员名称的字符数可能不同。

请注意,您可以直接使用 . 运算符访问名称的 length 方法,因为您在 if 分支内、进行 null 检查后访问 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 分支的主体中,添加一个 println 语句,接受字符串 "You didn't input a name."
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 分支内的 Body 1 假设变量不为 null。因此,在此主体中,您可以访问变量的方法或属性,就像它是一个非可空变量一样,无需使用 ?. 安全调用运算符或 !! 非空断言运算符。
  • else 分支内的 Body 2 假设变量为 null。因此,在此主体中,您可以添加在变量为 null 时应运行的语句。
  • 在 Body 1 和 Body 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
    }
}

0 用作名称为 null 时的默认值。

  1. main() 函数末尾,添加一个 println 语句,接受字符串 "The number of characters in your favorite actor's name is $lengthOfName.",然后运行该程序。
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
  • 要访问可空变量的方法或属性,需要使用 ?. 安全调用运算符或 !! 非空断言运算符。
  • 您可以使用带有 null 检查的 if/else 语句,在非可空上下文访问可空变量。
  • 您可以使用 if/else 表达式将可空变量转换为非可空类型。
  • 您可以使用 if/else 表达式或 ?: Elvis 运算符为可空变量为 null 时提供默认值。

了解详情