第63章:标准库函数——apply、let、run、with、also、takeIf、takeUnless、repeat
Kotlin 的标准库函数,说白了就是帮你省掉一堆样板代码的利器。我刚接触 Kotlin 时,看到 apply、let 这些函数,第一反应是「这不就是语法糖吗?」后来在项目中用顺手了,才发现它们能极大提升代码的可读性。今天咱们就把这八个函数掰开揉碎,看看它们各自适合什么场景。
一、作用域函数全家桶
apply、let、run、with、also 这五个函数,核心作用是在一个对象的上下文中执行代码块。它们之间的区别,主要在于「如何引用上下文对象」和「返回值是什么」。
| 函数 | 上下文对象引用 | 返回值 | 典型场景 |
|---|---|---|---|
| apply | this(接收者) | 上下文对象本身 | 对象配置、初始化 |
| let | it(参数) | Lambda 结果 | 非空判断、链式操作 |
| run | this(接收者) | Lambda 结果 | 对象转换、计算 |
| with | this(接收者) | Lambda 结果 | 对同一对象多次操作 |
| also | it(参数) | 上下文对象本身 | 附加操作、日志 |
我个人习惯这样记:apply 和 also 返回对象本身,适合链式调用;let、run、with 返回 Lambda 结果,适合转换值。你想想看,如果只是配置对象,用 apply 最顺手;如果要做非空判断,let 就是最佳选择。
二、apply:对象配置的瑞士军刀
apply 是我在项目中用得最多的函数。它让你在对象初始化后,还能像 Builder 模式一样继续配置属性。
val user = User().apply {
name = "张三"
age = 28
email = "zhangsan@example.com"
// 这里 this 就是 User 对象,可以省略
}
嗯,这里要注意:apply 的代码块里,this 就是上下文对象。你不需要写 user.name,直接写 name 就行。返回值是 User 对象本身,所以可以继续链式调用。
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse("https://example.com")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
三、let:非空判断的救星
let 函数最经典的用法,就是配合安全调用操作符 ?. 做非空判断。我曾经在项目中因为忘记判空,导致线上崩溃了好几次,后来养成了用 let 的习惯。
val name: String? = getUserName()
name?.let {
// 这里 it 就是非空的 String
println("用户名为:$it")
// 可以给 it 重命名
} ?: run {
println("用户名为空")
}
为什么会这样?因为 let 只在对象非空时执行,而且 Lambda 里的 it 已经被智能转换为非空类型。说白了,这就是 Kotlin 帮你省掉了一堆 if (name != null) 的判断。
// 不推荐:嵌套太多
user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
println(city)
}
}
}
// 推荐:用安全调用 + 空合并
println(user?.address?.city ?: "未知城市")
记住:let 不是万能的,简单场景用安全调用更清晰。
四、run:对象转换的利器
run 函数结合了 apply 和 let 的特点:上下文对象用 this 引用,返回值是 Lambda 结果。我常用它来做对象到字符串的转换,或者执行一段需要上下文的计算。
val user = User("李四", 30)
val description = user.run {
// this 就是 user
"姓名:$name,年龄:$age"
}
println(description) // 输出:姓名:李四,年龄:30
run 还有一个不带接收者的版本,就是直接调用 run 函数,适合执行一段代码块并返回结果:
val result = run {
val x = 10
val y = 20
x + y
}
println(result) // 30
五、with:同一对象的多次操作
with 函数和 run 很像,区别在于 with 是作为独立函数调用,需要传入上下文对象。我个人习惯在需要对同一个对象做多次操作时使用 with,这样代码更紧凑。
val user = User("王五", 25)
with(user) {
println("姓名:$name")
println("年龄:$age")
// 这里 this 就是 user
}
你想想看,如果不使用 with,你可能要写 user.name、user.age 很多次。with 让你省掉了重复的对象引用。
六、also:附加操作的完美选择
also 函数和 apply 一样返回对象本身,但上下文对象用 it 引用。这意味着你可以在 also 里做日志、校验等附加操作,而不影响主流程。
val user = User("赵六", 22).also {
// it 就是 User 对象
println("创建用户:${it.name}")
// 可以做日志记录
logService.record("用户创建成功")
}
我在项目中常用 also 来做调试日志,或者在执行某些操作前做校验。因为 also 返回对象本身,所以可以插在链式调用的中间位置。
七、takeIf 和 takeUnless:条件过滤
takeIf 和 takeUnless 是一对互补的函数。takeIf 在条件为 true 时返回对象本身,否则返回 null;takeUnless 正好相反。
val number = 42
val evenNumber = number.takeIf { it % 2 == 0 }
println(evenNumber) // 42
val oddNumber = number.takeUnless { it % 2 == 0 }
println(oddNumber) // null
嗯,这里要注意:takeIf 和 takeUnless 返回的是可空类型。我曾经在项目中用它们配合 let 做链式过滤,效果非常好:
val result = getUserList()
.filter { it.age > 18 }
.firstOrNull()
?.takeIf { it.name.startsWith("张") }
?.let { println("符合条件的用户:${it.name}") }
八、repeat:循环的语法糖
repeat 函数就是 for 循环的简化版,适合执行固定次数的操作。我个人觉得它最大的价值在于代码意图更明确——看到 repeat 就知道是「重复执行 N 次」。
repeat(5) { index ->
println("第 ${index + 1} 次执行")
}
你想想看,如果用传统的 for 循环,你要写 for (i in 0 until 5),而 repeat 直接告诉你「重复 5 次」。虽然功能一样,但代码的语义更强。
九、知识体系总览
为了让你更直观地理解这八个函数的关系,我画了一张图:
十、实战建议
说了这么多,最后给你一些实战中的选择建议:
- 配置对象:用 apply,返回对象本身,适合链式调用
- 非空判断:用 let,配合 ?. 操作符,代码简洁
- 对象转换:用 run,返回转换后的结果
- 多次操作:用 with,省掉重复的对象引用
- 附加操作:用 also,不影响主流程
- 条件过滤:用 takeIf 或 takeUnless,配合 let 使用
- 固定循环:用 repeat,语义明确
记住,这些函数不是银弹。我在项目中见过有人为了用而用,结果代码反而更难懂。核心原则是:让代码更清晰,而不是更花哨。如果你觉得用传统写法更直观,那就用传统写法。工具是为人服务的,不是反过来。
公众号:蓝海资料掘金营,微信deep3321