15、空安全:可空类型与不可空类型、安全调用操作符、Elvis操作符、非空断言、let函数

空指针异常(NullPointerException)——这大概是每个Java开发者心里永远的痛。我记得刚转Kotlin那会儿,最让我舒坦的就是终于不用再写一堆if (xxx != null)的判空代码了。Kotlin的空安全机制,说白了就是帮你把运行时可能炸掉的雷,提前在编译期就排掉。

这一章我们就把空安全这块彻底吃透。我会结合自己踩过的坑,把可空类型、安全调用、Elvis操作符、非空断言和let函数这些知识点串起来讲。

Kotlin 空安全知识体系 空安全 可空类型 vs 不可空类型 安全调用操作符 ?. Elvis 操作符 ?: 非空断言 !! let 函数 智能类型转换 目标:编译期消灭 NullPointerException

可空类型与不可空类型

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