Kotlin 空安全(Kotlin Null Safety)
简介
Kotlin 的空安全系统通过在类型系统中区分可空类型与非空类型,在编译期消除 NullPointerException。这种方式让空值处理变得显式,迫使开发者有意识地处理潜在的空值。
与 Java 中任何引用都可能为 null 不同,Kotlin 要求用 ? 运算符显式声明可空性。编译器在解引用可空值之前强制进行空检查,从而防止困扰 Java 应用的绝大多数空相关崩溃。
本技能涵盖可空类型、安全调用运算符、智能转换、泛型类型中的可空性,以及设计空安全 API 同时保持代码清晰度的模式。
可空类型
可空类型显式表示变量或属性可以持有 null,而非空类型在编译期保证非空值。
? 后缀使类型变为可空。非空类型在没有显式可空声明的情况下不能赋值为 null,从而防止意外的空引用。
安全调用运算符
安全调用运算符 ?. 安全地访问可空引用上的属性和方法,如果接收者为 null 则返回 null,而不是抛出 NPE。安全调用链在第一个 null 处短路,使深层嵌套的可选访问变得简洁安全,无需多次空检查。
Elvis 运算符与空值合并
Elvis 运算符 ?: 为 null 表达式提供默认值,无需冗长的 if-else 语句即可实现简洁的回退逻辑。Elvis 运算符仅在左侧为 null 时才对右侧求值,支持默认值的惰性求值。
智能转换
智能转换在空检查之后自动将可空类型转换为非空类型,消除冗余转换并提高代码清晰度。智能转换适用于不可变变量和 val 属性,但不适用于 var 属性,因为 var 可能在检查与使用之间发生变化。
非空断言与平台类型
非空断言运算符 !! 在值为 null 时显式抛出 NPE,适用于 null 不可能出现但编译器无法验证的情况。!! 运算符应谨慎使用,仅在你高度确信值非空时使用,因为它重新引入了崩溃的可能性。来自 Java 的平台类型(String!)在 Kotlin 中出于安全考虑被视为可空。
集合与泛型中的可空性
集合和泛型类型在容器和元素两个层面都支持可空性,需要清晰区分可空元素与可空集合。理解 List<String?>(可空字符串的列表)与 List<String>?(可空的列表)之间的区别对于正确的空值处理至关重要。
设计空安全 API
使用适当的可空性设计 API 可以提高可用性,并通过在类型系统中明确空值预期来防止误用。良好的 API 设计尽可能减少可空性,使用空集合代替 null 列表,并使用密封类实现更丰富的可选值语义。
最佳实践
默认优先使用非空类型,以最大化编译期安全性并减少整个代码库中的空检查;使用安全调用运算符 ?. 进行链式调用,而不是多次空检查,以保持代码简洁可读;使用 Elvis 运算符 ?: 提供默认值,而不是冗长的 if-else 链;返回空集合而不是 null,以简化客户端代码;在空检查后利用智能转换避免冗余转换;尽量减少 !! 运算符的使用,并在使用时记录假设;设计具有显式可空性的 API 以清晰传达意图;使用 lateinit 进行非空延迟初始化,而不是带手动空检查的可空属性;应用可空扩展提供 isNullOrEmpty() 等工具;在表示成功、错误和缺失状态时,优先使用密封类而非可空类型以获得更丰富的语义。
常见陷阱
在值不应为 null 时过度使用可空类型会使 API 更难使用并增加不必要的检查;链式使用 !! 运算符会创建不清晰的崩溃点;忽略来自 Java 的平台类型可能导致 NPE;对 var 属性使用智能转换会失败,因为 var 可能变化;返回 null 集合而不是空集合会迫使客户端处理两种独立情况;未考虑 equals/hashCode 中的可空性可能导致集合和比较中的意外行为;忘记安全调用返回可空类型会导致后续问题。