空安全
空安全是 Kotlin 的一项特性,旨在显著减少空引用的风险,也被称为 十亿美元的错误。
许多编程语言(包括 Java)中的一个常见陷阱是,访问空引用的成员会导致空引用异常。 在 Java 中,这等同于 NullPointerException
,简称 NPE。
Kotlin 明确支持可空性作为其类型系统的一部分,这意味着你可以显式声明哪些变量或属性允许为 null
。 此外,当你声明非空变量时,编译器会强制确保这些变量不能持有 null
值,从而防止出现 NPE。
Kotlin 的空安全通过在编译时捕获潜在的与 null
相关的问题,而不是在运行时捕获,从而确保代码的安全性。 此特性通过显式表达 null
值,改善了代码的稳健性、可读性和可维护性,使代码更易于理解和管理。
Kotlin 中可能导致 NPE 的原因只可能是以下几种:
使用 非空断言操作符
!!
。初始化期间的数据不一致,例如:
Java 互操作:
尝试访问
null
引用的成员,引用的是 平台类型。与泛型类型相关的可空性问题。例如,Java 代码片段将
null
添加到 Kotlin 的MutableList<String>
中,而正确的做法是使用MutableList<String?>
来处理。由外部 Java 代码引起的其他问题。
可空类型和非空类型
在 Kotlin 中,类型系统区分了可以保存 null
的类型(可空类型)和不能保存 null
的类型(非空类型)。 例如, String
类型的普通变量不能保存 null
:
你可以安全地调用方法或访问 a
的属性。由于 a
是一个非空变量,调用不会导致空指针异常(NPE)。 编译器确保 a
始终持有有效的 String
值,因此没有访问其属性或方法时发生空指针异常的风险:
为了允许 null
值,在变量类型后加上 ?
符号进行声明。例如,你可以通过写 String?
来声明一个可空字符串。 这个表达式使得 String
成为一个可以接受 null
的类型:
如果你尝试直接访问 b
的 length
属性,编译器会报错。这是因为 b
被声明为可空变量,可以保存 null
值。 直接在可空类型上访问属性会导致空指针异常(NPE):
在上述示例中,编译器要求你使用安全调用来检查是否为 null
,然后再访问属性或执行操作。处理可空类型有几种方式:
请继续阅读接下来的章节,了解更多关于 null
处理的工具和技巧。
使用 if
条件检查 null
在处理可空类型时,需要安全地处理空值以避免空指针异常(NPE)。一种方法是使用 if
条件表达式显式地检查空值。
例如,检查 b
是否为 null
,然后再访问 b.length
:
在上述示例中,编译器执行了 智能类型转换 ,将类型从可空的 String?
转换为非空的 String
。 它还跟踪你执行的检查,并允许在 if
条件内调用 length
。
更复杂的条件也受到支持:
请注意,上述示例仅在编译器可以保证 b
在检查和使用之间不会改变时才有效,这与 智能类型转换的前提条件 相同。
安全调用操作符
安全调用操作符 ?.
允许你以更简洁的方式安全地处理空值。 如果对象为 null
,则 ?.
操作符会直接返回 null
,而不是抛出空指针异常(NPE):
b?.length
表达式会检查是否为 null
,如果 b
不为 null
,则返回 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
。以下是用 if
条件表达式实现相同的安全调用:
你还可以将安全调用放在赋值语句的左侧:
在上述示例中,如果安全调用链中的任何接收者为 null
,赋值操作将被跳过,右侧的表达式根本不会被计算。 例如,如果 person
或 person.department
为 null
,函数将不会被调用。
Elvis 操作符
在处理可空类型时,你可以检查是否为 null
并提供一个替代值。 例如,如果 b
不为 null
,则访问 b.length
。否则,返回一个替代值:
你可以使用 Elvis 操作符 ?:
更简洁地处理这个问题,而不需要写完整的 if
表达式:
如果 Elvis 操作符左侧的表达式不为 null
,则返回它。 否则,Elvis 操作符返回右侧的表达式。右侧的表达式仅在左侧为 null
时才会被计算。
由于 throw
和 return
在 Kotlin 中也是表达式,你也可以将它们用于 Elvis 操作符的右侧。 例如,在检查函数参数时,这非常方便:
非空断言操作符
非空断言操作符 !!
将任何值转换为非空类型。
当你对一个值不为 null
的变量使用 !!
操作符时,它会被安全地处理为非空类型,代码会正常执行。 然而,如果该值为 null
, !!
操作符会强制它作为非空类型来处理,从而导致空指针异常 (NPE)。
当 b
不为 null
且 !!
操作符使其返回非空值(在这个例子中是 String
类型)时,它可以正确地访问 length
属性:
当 b
为 null
且 !!
操作符强制它返回非空值时,会发生空指针异常 (NPE):
!!
操作符特别有用,当你确信一个值不为 null
且没有发生空指针异常的风险时,但编译器无法保证这一点(由于某些规则)。 在这种情况下,你可以使用 !!
操作符显式地告诉编译器该值不为 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
类型,转换失败并安全地返回 null
。 它也打印了 "Hello, Kotlin!"
,因为它匹配了 String?
类型,安全转换成功。
可空类型的集合
如果你有一个包含可空元素的集合,并且希望只保留非空元素,可以使用 filterNotNull()
函数:
接下来做什么?
了解哪些泛型类型是 绝对非空的。