作用域函数
Kotlin 标准库提供了 5 个著名的作用域函数:let, run, with, apply, also。它们本质上都是内联的、带接收者的 高阶函数。
它们的共同目标是:在一个对象的上下文中执行代码块,从而使代码更加简洁、易读。
五大金刚:如何选择?
这 5 个函数非常相似,区别主要在于两点:
- 上下文对象如何引用:是
this还是it? - 返回值是什么:是对象本身还是Lambda 结果?
决策矩阵
| 函数 | 引用方式 | 返回值 | 典型用途 |
|---|---|---|---|
| apply | this | 对象本身 | 对象配置、初始化 |
| also | it | 对象本身 | 附加副作用(日志、打印),不打断链式调用 |
| let | it | Lambda 结果 | 空检查、将对象作为参数传递、变换 |
| run | this | Lambda 结果 | 复杂计算、作用域隔离 |
| with | this | Lambda 结果 | 对同一个对象执行多个操作(非扩展函数) |
详细实战
apply & also:返回对象本身
这两个函数适合处于调用链的中间,或者用于初始化。
apply (
this): “我要配置这个对象。”kotlinval dialog = Dialog(context).apply { setTitle("Warn") // this.setTitle() setCancelable(false) }also (
it): “我要用这个对象做点额外的事(且不改变它)。”kotlinval book = createBook() .also { println("Created book: ${it.name}") } // 打印日志 .also { repository.save(it) } // 保存
let & run:返回计算结果
这两个函数适合位于调用链的末尾,或者进行数据转换。
let (
it): 最常用于空安全调用?.let。kotlinval result = str?.let { println(it) it.length // 返回 Int }run (
this): 适合混合了对象配置与计算逻辑的场景。kotlinval width = view.run { measure() // this.measure() measuredWidth + padding // 返回计算结果 }
with:独立的调用
with 不是扩展函数,它接收一个对象作为参数。读起来像“使用这个对象,做以下事情”。
kotlin
with(settings) {
// 这里的 this 就是 settings
load()
applyChanges()
}契约 (contracts) 的隐式支持
你在使用 run 或 apply 初始化变量时,可能会发现编译器非常聪明:
kotlin
val service: Service
run {
service = ServiceImpl()
}
println(service) // 编译器知道 service 已经被初始化了!这得益于标准库在这些函数中使用了 Kotlin Contracts,明确告知了编译器:“这个 Lambda 块一定会执行,且只执行一次” 。虽然你不需要自己写契约,但了解这一点有助于你理解为什么 these 函数能无缝融入控制流分析。
条件执行:takeIf 与 takeUnless
这两个函数通常配合作用域函数使用,用于在链式调用中嵌入 if 逻辑。
takeIf { predicate }:如果满足条件返回this,否则返回null。takeUnless { predicate }:如果不满足条件返回this,否则返回null。
kotlin
// 只有在文件可读且非空时才读取
val content = File("data.txt")
.takeIf { it.exists() && it.canRead() }
?.readText()反模式:不要滥用
虽然作用域函数很酷,但过度使用会降低可读性。
- 避免嵌套:
apply套run套let,会导致this和it指向混乱。如果必须嵌套,请显式命名 Lambda 参数。 - 避免链式过长:调试时很难在链式调用的中间打断点。
- let vs if:如果只是简单的空检查,有时
if (x != null)比x?.let更清晰。
总结
- 配置对象 ->
apply - 附加操作 ->
also - 空检查/转换 ->
let - 复杂逻辑/计算 ->
run - 批量操作 ->
with