可空类型(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
引用。迄今为止,在本书中我们创建的所有var
和val
都自动是非可空的。 - [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
是可空的。
即使看起来我们只是在现有类型末尾添加 ?
来修改现有类型,实际上我们正在指定一种不同的类型。例如,String
和 String?
是两种不同的类型。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 找到。