第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 对象本身,所以可以继续链式调用。

我的经验:在 Android 开发中,apply 常用于配置 Intent、Dialog、Notification 等。比如:
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) 的判断。

避坑指南:我曾经在链式调用中滥用 let,导致代码可读性变差。比如:
// 不推荐:嵌套太多
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 让你省掉了重复的对象引用。

核心区别:with 和 run 几乎等价,但 with 是独立函数,run 是扩展函数。实际使用中,我更推荐用 run,因为 run 可以链式调用,而 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}") }
我的经验:takeIf 比传统的 if 判断更简洁,但不要滥用。如果条件逻辑很复杂,还是用传统的 if-else 更清晰。

八、repeat:循环的语法糖

repeat 函数就是 for 循环的简化版,适合执行固定次数的操作。我个人觉得它最大的价值在于代码意图更明确——看到 repeat 就知道是「重复执行 N 次」。

repeat(5) { index ->
    println("第 ${index + 1} 次执行")
}

你想想看,如果用传统的 for 循环,你要写 for (i in 0 until 5),而 repeat 直接告诉你「重复 5 次」。虽然功能一样,但代码的语义更强。

避坑指南:我曾经在项目中用 repeat 做复杂的业务逻辑,结果发现代码可读性很差。repeat 适合简单的重复操作,如果循环体很复杂,还是用 for 循环或者 while 更合适。

九、知识体系总览

为了让你更直观地理解这八个函数的关系,我画了一张图:

Kotlin 标准库函数体系 作用域函数(5个) apply 返回对象本身 let 返回 Lambda 结果 run 返回 Lambda 结果 with 返回 Lambda 结果 also 返回对象本身 条件与循环函数(3个) takeIf 条件为 true 返回对象 takeUnless 条件为 false 返回对象 repeat 重复执行 N 次

十、实战建议

说了这么多,最后给你一些实战中的选择建议:

  • 配置对象:用 apply,返回对象本身,适合链式调用
  • 非空判断:用 let,配合 ?. 操作符,代码简洁
  • 对象转换:用 run,返回转换后的结果
  • 多次操作:用 with,省掉重复的对象引用
  • 附加操作:用 also,不影响主流程
  • 条件过滤:用 takeIf 或 takeUnless,配合 let 使用
  • 固定循环:用 repeat,语义明确

记住,这些函数不是银弹。我在项目中见过有人为了用而用,结果代码反而更难懂。核心原则是:让代码更清晰,而不是更花哨。如果你觉得用传统写法更直观,那就用传统写法。工具是为人服务的,不是反过来。


公众号:蓝海资料掘金营,微信deep3321