Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

可空类型(Nullable Types)

考虑一个有时会产生“无结果”的函数。当发生这种情况时,函数本身不会产生错误。没有什么出错了,只是“没有答案”。

一个很好的例子是从 Map 中检索值。如果 Map 不包含给定键的值,它不能给您答案,因此返回一个 null 引用来表示“没有值”:

// NullableTypes/NullInMaps.kt
import atomictest.eq

fun main() {
  val map = mapOf(0 to "yes", 1 to "no")
  map[2] eq null
}

像 Java 这样的语言允许结果既可以是 null,也可以是有意义的值。不幸的是,如果您像对待有意义的值一样对待 null,会导致严重的失败(在 Java 中,这会产生 NullPointerException;在更原始的语言(如 C)中,null 指针可能会导致进程甚至操作系统或机器崩溃)。null 引用的创造者 Tony Hoare 将其称为“我十亿美元的错误”(尽管实际成本可能远远超过这个数字)。

解决此问题的一个可能方法是让语言从一开始就不允许 null,而是引入特殊的“没有值”指示器。Kotlin 可能会这样做,但必须与 Java 进行交互,而 Java 使用 null

Kotlin 的解决方案可能是最好的折衷方案:类型默认为非可空。但是,如果某个内容可能生成 null 结果,您必须在类型名称后添加问号,以明确标记该结果为可空:

// NullableTypes/NullableTypes.kt
import atomictest.eq

fun main() {
  val s1 = "abc"             // [1]

  // 编译时错误:
  // val s2: String = null   // [2]

  // 可空定义:
  val s3: String? = null     // [3]
  val s4: String? = s1       // [4]

  // 编译时错误:
  // val s5: String = s4     // [5]
  val s6 = s4                // [6]

  s1 eq "abc"
  s3 eq null
  s4 eq "abc"
  s6 eq "abc"
}
  • [1] s1 不能包含 null 引用。迄今为止,在本书中我们创建的所有 varval 都自动是非可空的。
  • [2] 错误消息为:null can not be a value of a non-null type String
  • [3] 要定义一个可以包含 null 引用的标识符,需要在类型名称的末尾加上 ?。这种标识符可以包含 null 或常规值。
  • [4] 可以在可空类型中存储 null 和常规非可空值。
  • [5] 不能将可空类型的标识符赋值给非可空类型的标识符。Kotlin 会发出:Type mismatch: inferred type is String? but String was expected. 即使实际值在这种情况下是非空的(我们知道它是 "abc"),Kotlin 也不允许,因为它们是两种不同的类型。
  • [6] 如果使用类型推断,Kotlin 会生成适当的类型。在这里,s6 是可空的,因为 s4 是可空的。

即使看起来我们只是在现有类型末尾添加 ? 来修改现有类型,实际上我们正在指定一种不同的类型。例如,StringString? 是两种不同的类型。String? 类型禁止了第 [2][5] 行中的操作,从而保证非可空类型的值永远不会是 null

使用方括号从 Map 中检索值会产生一个可空结果,因为底层的 Map 实现来自 Java:

// NullableTypes/NullableInMap.kt
import atomictest.eq

fun main() {
  val map = mapOf(0 to "yes", 1 to "no")
  val first: String? = map[0]
  val second: String? = map[2]
  first eq "yes"
  second eq null
}

为什么知道值不可能是 null 是重要的?许多操作隐式地假设非可空的结果。例如,如果接收方值为 null,则调用成员函数将导致异常。在 Java 中,这样的调用将导致 NullPointerException(通常简称为 NPE)。由于几乎任何值都可以是 null,所以任何函数调用都可能以这种方式失败。在这些情况下,您必须编写代码检查 null 结果,或依赖代码的其他部分来防范 null

在 Kotlin 中,您不能简单地将可空类型的值解引用(调用成员函数或访问成员属性):

// NullableTypes/Dereference.kt
import atomictest.eq

fun main() {
  val s1: String = "abc"
  val s2: String? = s1

  s1.length eq 3         // [1]
  // 不能编译:
  // s2.length           // [2]
}

可以像 [1] 中一样访问非可空类型的成员。如果引用可空类型的成员,就像 [2] 中一样,Kotlin 将会发出错误。

大多数类型的值都存储为对内存中对象的引用。这就是术语解引用的含义——要访问一个对象,您需要从内存中检索它的值。

确保解引用可空类型不会抛出 NullPointerException 的最直接的方法是明确检查引用是否不为 null

// NullableTypes/ExplicitCheck.kt
import atomictest.eq

fun main() {
  val s: String? = "abc"
  if (s != null)
    s.length eq 3
}

在显式的 if 检查之后,Kotlin 允许您解引用可空类型。但是,在处理可空类型时,每次编写此 if 都会显得太嘈杂。Kotlin 有简洁的语法来缓解这个问题,您将在随后的章节中了解到。

每当创建一个新类时,Kotlin 会自动包含可空类型和非可空类型:

// NullableTypes/Amphibian.kt
package nullabletypes

class Amphibian

enum class Species {
  Frog, Toad, Salamander, Caecilian
}

fun main() {
  val a1: Amphibian = Amphibian()
  val a2: Amphibian? = null
  val at1: Species = Species.Toad
  val at2: Species? = null
}

正如您所看到的,我们没有特别做什么来产生补充的可空类型——它们默认可用。

练习和答案可以在 www.AtomicKotlin.com 找到。