第18章:作用域函数——let、run、with、apply、also的区别与使用场景、链式调用
作用域函数,说白了就是Kotlin给咱们提供的一组「语法糖」。它们让你能在某个对象的上下文中执行代码块,省去重复写变量名的麻烦。我刚接触Kotlin时,觉得这五个函数长得都差不多,用起来也迷迷糊糊的。后来踩了不少坑,才慢慢摸清它们的脾气。
今天我就把这五个函数掰开揉碎,讲清楚它们的区别、适用场景,以及怎么优雅地链式调用。
五个函数的核心区别
要理解它们,你只需要记住两个维度:上下文对象的引用方式和返回值。
| 函数 | 上下文对象引用 | 返回值 | 典型场景 |
|---|---|---|---|
| let | it | Lambda结果 | 非空判断、局部变量作用域限定 |
| run | this | Lambda结果 | 对象配置+计算结果 |
| with | this | Lambda结果 | 对同一对象做多次操作 |
| apply | this | 上下文对象本身 | 对象初始化配置 |
| also | it | 上下文对象本身 | 附加操作、日志打印 |
嗯,这张表你得记牢。我个人习惯把它贴在显示器边上,写代码时扫一眼就清楚了。
逐个拆解,讲透每个函数
1. let——我最常用的非空处理工具
let 用 it 引用对象,返回Lambda的最后一行的结果。它最常见的用法就是配合 ?. 做非空判断。
val name: String? = "Kotlin"
name?.let {
println("名字长度:${it.length}") // 只有非空时才执行
}
我在项目中遇到过这样的场景:从网络请求拿到一个用户对象,需要做一系列非空校验。用 let 嵌套,代码干净多了。
user?.let { u ->
println(u.name)
println(u.age)
}
2. run——配置+计算二合一
run 用 this 引用对象,返回Lambda结果。它和 let 的区别在于:run 里你可以直接调用对象的属性和方法,不用写 it.。
val result = StringBuilder().run {
append("Hello, ")
append("Kotlin!")
toString() // 返回字符串
}
println(result) // 输出:Hello, Kotlin!
说白了,run 就是「先配置,再计算」。我经常用它来构建一个临时对象并立即获取计算结果。
3. with——老朋友,但用得少
with 不是扩展函数,它把对象作为参数传进去。用 this 引用,返回Lambda结果。
val numbers = mutableListOf(1, 2, 3)
val sum = with(numbers) {
add(4)
add(5)
sum() // 返回总和
}
println(sum) // 输出:15
说实话,with 我用的不多。因为 run 基本能覆盖它的场景,而且 run 是扩展函数,写起来更自然。不过如果你已经有一个对象,想对它做多次操作并返回结果,with 也挺合适。
4. apply——对象配置的王者
apply 用 this 引用,返回对象本身。这是我最喜欢的函数之一,特别适合初始化对象。
val dialog = AlertDialog.Builder(context).apply {
setTitle("提示")
setMessage("确定要删除吗?")
setPositiveButton("确定") { _, _ -> }
setNegativeButton("取消") { _, _ -> }
}.create()
你看,apply 返回的是 Builder 本身,所以可以继续链式调用。我在项目中用 apply 配置 View、Fragment、网络请求参数,简直不要太爽。
5. also——低调的「顺便做点事」
also 用 it 引用,返回对象本身。它和 apply 的区别在于:also 用 it,apply 用 this。
val list = mutableListOf(1, 2, 3).also {
println("初始化列表:$it") // 打印日志
it.add(4) // 顺便加个元素
}
我曾经在调试时,用 also 在链式调用中间插入日志,不用打断原有的逻辑流。嗯,这个用法很实用。
链式调用——把函数串起来用
作用域函数真正的威力在于链式调用。你可以把多个操作串在一起,代码读起来像流水线一样顺畅。
data class User(var name: String, var age: Int, var email: String)
val user = User("", 0, "").apply {
name = "张三"
age = 28
}.also {
println("用户信息已初始化:$it")
}.let {
if (it.age > 18) "成年人" else "未成年人"
}
println(user) // 输出:成年人
这个例子展示了:先用 apply 配置对象,再用 also 打印日志,最后用 let 转换结果。每一步都清晰明了。
- 需要配置对象并返回本身 → apply
- 需要附加操作(日志、校验)→ also
- 需要非空判断或转换结果 → let
- 需要配置+计算并返回结果 → run
- 已有对象,需要多次操作 → with
避坑指南
我曾经在代码里嵌套了三层作用域函数,结果自己都看不懂了。后来我总结了几条经验:
- 不要嵌套超过两层。超过两层,可读性急剧下降。这时候应该抽成独立函数。
- 注意 this 的隐式调用。在
run和apply里,this指向上下文对象,容易和外部作用域的this混淆。如果类里也有同名属性,会出问题。 - 链式调用不要太长。超过4-5个链式调用,建议拆成多行,每行一个操作。
知识体系图
下面这张图帮你理清五个函数的关系和选择逻辑:
实战建议
说了这么多,最后给你几条我个人的实战建议:
- 团队统一风格。如果团队里有人习惯用
apply,有人用also,代码会很难看。建议在代码规范里统一约定。 - 优先用 apply 和 let。这两个函数覆盖了80%的场景。其他三个函数,等你真正需要时再用。
- 不要为了用而用。如果一段代码用普通写法更清晰,就别强行套作用域函数。代码是给人读的,不是炫技的。
好了,作用域函数就讲到这里。你想想看,是不是比之前清晰多了?下次写代码时,试着用这些函数重构一下,你会发现代码变得更简洁、更Kotlin了。
公众号:蓝海资料掘金营,微信deep3321