5.9 空安全
9 分钟阅读
5.9 空安全
空安全是 Kotlin 的一项特性,旨在显著降低空引用(也被称为价值十亿美元的错误)的风险。
在许多编程语言(包括 Java)中,最常见的陷阱之一是访问空引用的成员会导致空引用异常。在 Java 中,这相当于 NullPointerException,简称 NPE。
Kotlin 在类型系统中显式支持可空性,也就是说你可以明确声明哪些变量或属性允许为 null。此外,当你声明非空变量时,编译器会强制这些变量不能持有 null 值,从而避免 NPE。
Kotlin 的空安全通过在编译期而非运行期捕获潜在的空相关问题来确保代码更安全。这一特性通过显式表达 null 值,提高了代码的健壮性、可读性和可维护性,让代码更易于理解和管理。
Kotlin 中可能产生 NPE 的原因只有:
- 显式调用
throw NullPointerException()。 - 使用非空断言运算符
!!。 - 初始化期间的数据不一致,例如:
- 在构造函数中可用的未初始化
this被用在其他地方(“泄露的this”)。 - 超类构造函数调用了某个 open 成员,而该成员在派生类中的实现使用了未初始化的状态。
- Java 互操作:
- 试图访问平台类型的
null引用的成员。 - 泛型类型的可空性问题。例如,一段 Java 代码向 Kotlin 的
MutableList<String>中添加了null,而正确处理它需要MutableList<String?>。 - 由外部 Java 代码引起的其他问题。
提示: 除 NPE 外,另一个与空安全相关的异常是
UninitializedPropertyAccessException。当你尝试访问尚未初始化的属性时,Kotlin 会抛出该异常,从而确保非空属性在准备就绪之前不会被使用。这通常发生在lateinit属性上。
可空类型与非空类型
在 Kotlin 中,类型系统区分可以持有 null 的类型(可空类型)和不能持有 null 的类型(非空类型)。例如,普通的 String 类型变量不能持有 null:
| |
你可以安全地调用 a 上的方法或访问其属性。由于 a 是非空变量,可以保证不会引发 NPE。编译器确保 a 始终持有有效的 String 值,因此不存在它在 null 时被访问属性或方法的风险:
| |
要允许 null 值,请在变量类型之后立即加上 ? 符号来声明变量。例如,你可以写成 String? 来声明可空字符串。这个写法让 String 成为可以接受 null 的类型:
| |
如果你尝试直接在 b 上访问 length,编译器会报错。这是因为 b 被声明为可空变量,可以持有 null 值。直接尝试访问可空对象上的属性会导致 NPE:
| |
在上面的例子中,编译器要求你在访问属性或执行操作之前,先用安全调用检查可空性。有几种方式可以处理可空值:
关于 null 处理工具和技巧的细节与示例,请阅读接下来的各节。
用 if 条件检查 null
处理可空类型时,你需要安全地处理可空性以避免 NPE。其中一种方式是用 if 条件表达式显式检查可空性。
例如,先检查 b 是否为 null,然后访问 b.length:
| |
在上面的例子中,编译器执行了智能转换,把类型从可空的 String? 变为非空的 String。它还会跟踪你所执行的检查信息,并允许在 if 条件内部调用 length。
也支持更复杂的条件:
| |
注意,上面的例子只有在编译器能够保证 b 在检查与使用之间不会改变时才有效,这与智能转换的前提条件相同。
安全调用运算符
安全调用运算符 ?. 让你可以用更简短的形式安全地处理可空性。如果对象为 null,?. 运算符不会抛出 NPE,而只是返回 null:
| |
表达式 b?.length 检查可空性:如果 b 非空则返回 b.length,否则返回 null。该表达式的类型是 Int?。
在 Kotlin 中,?. 运算符可以用于 var 和 val 变量:
- 可空的
var可以持有null(例如var nullableValue: String? = null)或非空值(例如var nullableValue: String? = "Kotlin")。如果它当前是非空值,你可以随时把它改为null。 - 可空的
val可以持有null(例如val nullableValue: String? = null)或非空值(例如val nullableValue: String? = "Kotlin")。如果它当前是非空值,之后就不能把它改为null。
安全调用在链式调用中很有用。例如,Bob 是一名员工,他可能被分配到某个部门(也可能没有)。而这个部门又可能有另一位员工作为部门主管。要获取 Bob 的部门主管的姓名(如果存在),你可以这样写:
| |
只要链中任何一个属性为 null,这条链就会返回 null。
你也可以把安全调用放在赋值语句的左侧:
| |
在上面的例子中,如果安全调用链中的某个接收者为 null,赋值就会被跳过,右侧的表达式根本不会被求值。例如,如果 person 或 person.department 为 null,该函数就不会被调用。下面是同一个安全调用用 if 条件表达的等价写法:
| |
Elvis 运算符
处理可空类型时,你可以检查是否为 null 并提供替代值。例如,如果 b 不为 null,就访问 b.length;否则返回一个替代值:
| |
你可以用 Elvis 运算符 ?: 更简洁地处理,而不必写出完整的 if 表达式:
| |
如果 ?: 左侧的表达式不为 null,Elvis 运算符就返回它;否则返回右侧的表达式。只有当左侧为 null 时,右侧表达式才会被求值。
由于 throw 和 return 在 Kotlin 中是表达式,你也可以把它们用在 Elvis 运算符的右侧。例如,在检查函数参数时这会很方便:
| |
非空断言运算符
非空断言运算符 !! 会把任何值转换为非空类型。
当你对值不为 null 的变量使用 !! 运算符时,它会被安全地当作非空类型处理,代码正常执行。不过,如果该值为 null,!! 运算符会强制把它当作非空处理,从而导致 NPE。
当 b 不为 null 时,!! 运算符让它返回其非空值(在本例中是 String),于是可以正确访问 length:
| |
当 b 为 null 时,!! 运算符让它返回其非空值,于是发生 NPE:
| |
当你确信某个值不为 null、不可能发生 NPE,但由于某些规则编译器无法保证时,!! 运算符特别有用。在这种情况下,你可以用 !! 运算符显式告诉编译器该值不为 null。
可空接收者
你可以使用接收者类型为可空类型的扩展函数,从而可以在可能为 null 的变量上调用这些函数。
通过在可空接收者类型上定义扩展函数,你可以在函数内部处理 null 值,而不必在每个调用处都检查 null。
例如,.toString() 扩展函数可以在可空接收者上调用。当在 null 值上调用时,它会安全地返回字符串 "null",而不会抛出异常:
| |
在上面的例子中,即使 person 为 null,.toString() 函数也会安全地返回字符串 "null"。这对调试和日志记录很有帮助。
如果你希望 .toString() 函数返回可空字符串(要么是字符串表示,要么是 null),请使用安全调用运算符 ?.。?. 运算符只在对象不为 null 时调用 .toString(),否则返回 null:
| |
?. 运算符让你在安全处理潜在 null 值的同时,仍然可以访问可能为 null 的对象的属性或函数。
let 函数
要处理 null 值并只对非空类型执行操作,你可以把安全调用运算符 ?. 与 let 函数结合使用。
这种组合适合对表达式求值、检查结果是否为 null,并只在结果不为 null 时执行代码,从而避免手动空检查:
| |
安全转换
用于类型转换的常规 Kotlin 运算符是 as。不过,如果对象不是目标类型,常规转换会抛出异常。
你可以使用 as? 运算符进行安全转换。它会尝试把值转换为指定类型,如果该值不属于该类型则返回 null:
| |
上面的代码打印 null,因为 a 不是 Int,所以转换安全地失败了。它还会打印 "Hello, Kotlin!",因为它匹配 String? 类型,所以安全转换成功。
元素可空的集合
如果你有一个元素可空的集合,并且只想保留非空元素,请使用 filterNotNull() 函数:
| |
接下来学什么?
- 学习如何在 Java 和 Kotlin 中处理可空性。
- 了解确定非空的泛型类型。