卷 II · CH 04 · 深度 04/18

Kotlin 的价值,不是少写分号

它真正减少的是「本来能通过编译、最后才在用户手机上炸掉」的状态空间。

NULL SAFETYEXPRESSIONSSCOPE FUNCTIONS
▷ 先答一下

下面哪一种写法最适合表达「名字为空时直接返回,否则继续拿到非空名字」?

  1. name!!
  2. name?.let { ... } 套住后面整段函数
  3. val safeName = name ?: return
  4. 先把 name 赋给可变全局变量

类型系统是在删路径

StringString? 是不同类型,意味着调用者必须在编译期处理「没有值」这条路径。安全调用 ?. 会把空传播下去;Elvis 运算符 ?: 给出替代值或提前退出;!! 则是你向编译器签字:「这里不可能为空,错了就崩。」

平台类型是 Java 互操作留下的洞,例如 Java 返回的 String!。它既可当非空也可当可空,编译器替你做不了决定。边界处尽早归一化:校验、转换成可空类型,或包成明确的领域结果,别让平台类型流进核心逻辑。

可以带走的判断:Kotlin 的安全不是不崩,而是迫使不确定性尽早出现在类型签名上。

表达式让分支必须产出值

ifwhen 都是表达式。把 UI 状态写成一个 when 的结果,编译器能检查每个分支是否返回同一种东西;配合 sealed 类型,还能检查是否漏了新状态。这比先声明一个可空变量、在多个分支里修改它更容易证明完整。

val label = when (status) {
    MessageStatus.Pending -> "发送中"
    MessageStatus.Sent -> "已发送"
    is MessageStatus.Failed -> "重试:${status.reason}"
}

data class 自动生成结构相等、copy 与解构,但 copy 是浅拷贝。内部若放可变列表,两个实例仍可能共享同一份列表。不可变状态需要的是不可变使用方式,不是一个注解或关键字。

五个作用域函数只问两件事

别背口诀,问两件事:对象在块里叫 this 还是 it?表达式返回对象本身,还是块的结果?

函数对象名返回常见用途
letit块结果变换、处理可空值
runthis块结果对象上下文里计算
withthis块结果对已有对象做一组读取
applythis对象配置对象
alsoit对象旁路日志或校验

嵌套三层 let 通常不是「很 Kotlin」,而是名字丢了。此时用有意义的局部变量和提前返回更清楚。扩展函数也不是给类加方法;它是静态分派的语法糖,不能访问私有成员,具体调用由接收者的编译期类型决定。

⌨ 自己跑一遍

观察空值提前退出、结构相等与引用相等。输出应明确区分 =====

display=Ada
structural=true
referential=false
missing=<anonymous>
data class User(val id: String, val name: String?)

fun displayName(user: User?): String {
    val name = user?.name?.trim()?.takeIf { it.isNotEmpty() }
        ?: return "<anonymous>"
    return name
}

fun main() {
    val a = User("1", " Ada ")
    val b = a.copy()
    println("display=${displayName(a)}")
    println("structural=${a == b}")
    println("referential=${a === b}")
    println("missing=${displayName(null)}")
}

粘到 Kotlin Playground。然后把 ?: return 换成 !!,传入 null,让风险从类型分支变成运行时崩溃。

▸ 在现实里

服务端字段可缺失、Room 迁移前数据不完整、Intent 参数来自旧通知——这些边界才是空值的产地。把 DTO 转成领域模型时一次处理,Compose 层就不必满屏 ?.,也不会在不同页面对同一缺失值给出不同解释。

✗ 这个直觉是错的

「Kotlin 空安全消灭了 NPE。」!!、平台类型、初始化顺序与 Java 互操作都能带回 NPE。

Kotlin 把大多数空值路径显式化;你的责任是在系统边界收窄它,而不是用 !! 把编译器闭嘴。

◇ 面试收口

答案是第 3 项。Elvis 加提前返回把剩余作用域里的值收窄为非空,控制流最直。let 适合短变换,不适合为了避空把整段业务嵌进去;!! 只是延迟风险。

这一章的一句话

好 Kotlin 不是更短,而是让不可能与不确定在类型里可见。

下一章把这句话推到底:一个消息同时是 isSending=trueisFailed=trueserverId=null 时,到底是什么意思?用类型改写后,这种组合会直接无法编译。