空值安全
可空类型与非空类型
Kotlin 的类型系统旨在消除空引用的危险,这也被称为价值十亿美元的错误。
在包括 Java 在内的许多编程语言中,最常见的陷阱之一是访问一个空引用的成员会导致空引用异常。 在 Java 中,这相当于 NullPointerException
,或者简称为 NPE。
Kotlin 中唯一可能导致 NPE 的原因是:
显式调用
throw NullPointerException()
。使用下面描述的
!!
操作符。初始化数据不一致,例如:
一个在构造函数中未初始化的
this
被传递并在某处使用(“泄漏的this
”)。父类构造函数调用一个 open 成员 ,其在派生类中的实现使用了未初始化的状态。
Java 互操作性:
尝试访问一个 平台类型 的
null
引用的成员;使用泛型进行 Java 互操作时的空值问题。 例如,一段 Java 代码可能向 Kotlin 的
MutableList<String>
中添加null
,因此需要一个MutableList<String?>
来处理它。由外部 Java 代码引起的其他问题。
在 Kotlin 中,类型系统区分了可以持有 null
的引用(可空引用)和不能持有 null
的引用(非空引用)。 例如,类型为 String
的普通变量不能持有 null
:
为了允许 null
,你可以通过写 String?
来声明一个可空字符串变量:
现在,当你调用 a
的方法或访问它的属性,可以保证不会导致 NPE,所以你可以安全地编写:
但是如果你想访问 b
的同一个属性,这样做是不安全的,编译器会报错:
但是你仍然需要访问那个属性,对吧?有几种方法可以做到这一点。
检查条件中的 null
首先,你可以显式检查 b
是否为 null
,并分别处理这两种情况:
编译器会跟踪你执行的检查信息,并允许在 if
内调用 length
。 更复杂的条件也是支持的:
注意,这仅在 b
是不可变的情况下有效(即,它是一个在检查和使用之间没有被修改的局部变量,或者它是一个有幕后字段且不可重写的成员 val
)。 否则,在检查之后 b
可能会变为 null
。
安全调用
访问可空变量的属性的第二种选择,是使用安全调用操作符 ?.
:
如果 b
不为 null,则返回 b.length
,否则返回 null
。这个表达式的类型是 Int?
。
安全调用在链式调用中非常有用。 例如,Bob 是一个员工,可能会被分配到一个部门(也可能不会)。 那个部门又可能有另一个员工担任部门主管。为了获取 Bob 部门主管的名字(如果有的话),你可以这样写:
这样的链式调用在其中任何一个属性为 null
时都会返回 null
。
为了仅对非空值执行特定操作,你可以将安全调用操作符与 let
一起使用:
安全调用也可以放在赋值的左侧。如果安全调用链中的一个接收者为 null
,赋值会被跳过,并且右侧的表达式根本不会被求值:
可空接收者
扩展函数可以定义在可空接收者上。 这样,你可以为 null
值指定行为,而无需在每个调用点使用空值检查逻辑。
例如, toString()
函数定义在一个可空接收者上。 它返回字符串 "null"(而不是 null
值)。这在某些情况下非常有用,例如日志记录:
如果你希望 toString()
调用返回一个可空字符串,请使用安全调用操作符 ?.
:
Elvis 运算符
当你有一个可空引用 b
时,你可以这样说:“如果 b
不为 null
,使用它,否则使用某个非空值”:
不用写完整的 if
表达式,你也可以使用 Elvis 运算符 ?:
来表达这个意思:
如果 ?:
左侧的表达式不为 null
,Elvis 运算符返回它本身的值,否则,返回右侧的表达式。 注意,只有当左侧为 null
时,右侧的表达式才会被求值。
由于 throw
和 return
在 Kotlin 中也是表达式,因此它们也可以用在 Elvis 运算符的右侧。这在检查函数参数时非常方便,例如:
!!
操作符
第三种选择是为 NPE 爱好者准备的:非空断言操作符(!!
)将任何值转换为非空类型,如果值为 null
,则抛出异常。 你可以写 b!!
,这将返回 b
的非空值(例如,我们的示例中的 String
),或者如果 b
为 null
则抛出 NPE:
因此,如果你想要一个 NPE,你可以获得它,但你必须明确要求它,它不会突然出现。
安全类型转换
普通的类型转换如果对象不是目标类型可能会导致 ClassCastException
。 另一种选择是使用安全类型转换,它在尝试失败时返回 null
:
可空类型的集合
如果你有一个可空类型元素的集合,并且想要过滤掉非空元素,可以使用 filterNotNull
:
接下来做什么?
了解绝对非空类型的泛型。