15、空安全:可空类型与不可空类型、安全调用操作符、Elvis操作符、非空断言、let函数
空指针异常(NullPointerException)——这大概是每个Java开发者心里永远的痛。我记得刚转Kotlin那会儿,最让我舒坦的就是终于不用再写一堆if (xxx != null)的判空代码了。Kotlin的空安全机制,说白了就是帮你把运行时可能炸掉的雷,提前在编译期就排掉。
这一章我们就把空安全这块彻底吃透。我会结合自己踩过的坑,把可空类型、安全调用、Elvis操作符、非空断言和let函数这些知识点串起来讲。
可空类型与不可空类型
Kotlin的类型系统把「空」这件事摆到了明面上。一个变量能不能为null,在声明的时候就得说清楚。
// 不可空类型 —— 绝对不能为 null
var name: String = "Kotlin"
name = null // 编译错误!
// 可空类型 —— 用 ? 标记
var nullableName: String? = "Kotlin"
nullableName = null // 没问题
我个人习惯是:能用不可空就不用可空。你想想看,一个变量如果永远不可能是null,那后面所有操作都不需要判空,多省心。
核心原则:Kotlin中,类型后面的?就是「允许为空」的通行证。没有?,编译器就帮你盯着,绝对不让null溜进去。
安全调用操作符 ?.
有了可空类型,怎么安全地访问它的属性或方法?用?.。如果对象为null,整个表达式返回null,不会崩。
val str: String? = "Hello"
val length: Int? = str?.length // 如果str为null,length就是null
// 链式调用
val city: String? = user?.address?.city // 中间任何一个为null,结果就是null
我在项目中遇到过这样的场景:从后端接口拿到的用户信息,可能某些字段是空的。用?.链式调用,一行代码搞定,再也不用写三层if嵌套了。
Elvis 操作符 ?:
Elvis操作符长得像表情符号?:,它的作用是:如果左边表达式不为null,就用左边的值;否则用右边的默认值。
val name: String? = null
val displayName = name ?: "匿名用户" // 结果为"匿名用户"
// 也可以配合 throw 使用
val age = inputAge ?: throw IllegalArgumentException("年龄不能为空")
小技巧:Elvis操作符和let函数经常搭配使用,后面会讲到。我个人觉得?:比Java的三元运算符更直观——「要么用这个,要么用那个」。
非空断言 !!
!!操作符的意思是:「我确定这个值不是null,出了事我负责」。它会把可空类型强制转为不可空类型,但如果值为null,立刻抛出NullPointerException。
val str: String? = "Kotlin"
val length: Int = str!!.length // 强制认为str不为null
// 如果str是null,这里就炸了
val danger: String? = null
val len = danger!!.length // 抛出 NullPointerException
警告:我曾经在代码里滥用!!,结果线上出了空指针。说白了,!!就是告诉编译器「别管我」,等于放弃了Kotlin的空安全检查。能用?.或?:解决,就别用!!。
let 函数
let是Kotlin标准库提供的一个作用域函数。配合安全调用操作符,可以优雅地处理可空对象:只在对象不为null时才执行代码块。
val name: String? = "Kotlin"
name?.let {
// 这里面的 it 就是非空的 String 类型
println("名字长度:${it.length}")
}
// 等价于:
if (name != null) {
println("名字长度:${name.length}")
}
你想想看,用?.let比写if判空简洁多了。而且let内部的作用域是独立的,不会污染外部变量名。
// 实际项目中的常见用法
user?.let { u ->
// u 已经是不可空类型
updateUI(u.name, u.age)
saveToDatabase(u)
}
最佳实践:我个人习惯用?.let替代if (xxx != null)的判空块。尤其是链式调用多个可空对象时,let能让代码更扁平、更易读。
智能类型转换
Kotlin编译器很聪明。一旦你做了非空判断,在判断的作用域内,编译器会自动把可空类型当成不可空类型来用。
val str: String? = "Hello"
if (str != null) {
// 这里 str 自动变成了 String 类型,不需要 ?. 或 !!
println(str.length)
}
// 或者用安全转换后
val result = str as? Int // 如果转换失败,result为null,不会抛异常
嗯,这里要注意:智能转换只在变量没有被修改的情况下生效。如果变量是var且可能在判断后被修改,编译器会要求你显式处理。
综合示例:用户信息展示
最后,我们看一个综合例子。假设要从数据库或网络获取用户信息,字段可能为空,需要安全地展示到UI上。
data class User(
val name: String?,
val age: Int?,
val email: String?
)
fun displayUserInfo(user: User?) {
// 安全调用 + Elvis + let 的组合拳
user?.let { u ->
val displayName = u.name ?: "未设置昵称"
val displayAge = u.age?.toString() ?: "年龄未知"
val displayEmail = u.email ?: "未绑定邮箱"
println("用户信息:$displayName, $displayAge, $displayEmail")
} ?: run {
println("用户数据为空")
}
}
这段代码里,?.let确保user不为null才执行,?:给每个字段提供默认值,?: run处理user本身为null的情况。我在实际项目中就是这么写的,既安全又清晰。
总结一下:
- 可空类型用
?标记,不可空类型默认 - 安全调用
?.避免空指针,结果为null就返回null - Elvis
?:提供默认值,简洁优雅 - 非空断言
!!慎用,除非你100%确定不为null - let函数配合
?.,在非空作用域内安全操作 - 智能转换让编译器帮你省掉多余的判空代码
空安全是Kotlin最实用的特性之一。刚开始可能觉得多写个?有点麻烦,但用习惯了就会发现——编译期多操一份心,运行时少炸一个雷。值。
公众号:蓝海资料掘金营,微信deep3321