第18章:作用域函数——let、run、with、apply、also的区别与使用场景、链式调用

作用域函数,说白了就是Kotlin给咱们提供的一组「语法糖」。它们让你能在某个对象的上下文中执行代码块,省去重复写变量名的麻烦。我刚接触Kotlin时,觉得这五个函数长得都差不多,用起来也迷迷糊糊的。后来踩了不少坑,才慢慢摸清它们的脾气。

今天我就把这五个函数掰开揉碎,讲清楚它们的区别、适用场景,以及怎么优雅地链式调用。

五个函数的核心区别

要理解它们,你只需要记住两个维度:上下文对象的引用方式返回值

函数 上下文对象引用 返回值 典型场景
let it Lambda结果 非空判断、局部变量作用域限定
run this Lambda结果 对象配置+计算结果
with this Lambda结果 对同一对象做多次操作
apply this 上下文对象本身 对象初始化配置
also it 上下文对象本身 附加操作、日志打印

嗯,这张表你得记牢。我个人习惯把它贴在显示器边上,写代码时扫一眼就清楚了。

逐个拆解,讲透每个函数

1. let——我最常用的非空处理工具

letit 引用对象,返回Lambda的最后一行的结果。它最常见的用法就是配合 ?. 做非空判断。

val name: String? = "Kotlin"
name?.let {
    println("名字长度:${it.length}")  // 只有非空时才执行
}

我在项目中遇到过这样的场景:从网络请求拿到一个用户对象,需要做一系列非空校验。用 let 嵌套,代码干净多了。

小技巧:let 的 it 可以重命名,提高可读性。
user?.let { u ->
    println(u.name)
    println(u.age)
}

2. run——配置+计算二合一

runthis 引用对象,返回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 也挺合适。

注意:with 不能用于可空类型。如果你需要处理可空对象,用 run 或 let 更安全。

4. apply——对象配置的王者

applythis 引用,返回对象本身。这是我最喜欢的函数之一,特别适合初始化对象。

val dialog = AlertDialog.Builder(context).apply {
    setTitle("提示")
    setMessage("确定要删除吗?")
    setPositiveButton("确定") { _, _ -> }
    setNegativeButton("取消") { _, _ -> }
}.create()

你看,apply 返回的是 Builder 本身,所以可以继续链式调用。我在项目中用 apply 配置 View、Fragment、网络请求参数,简直不要太爽。

5. also——低调的「顺便做点事」

alsoit 引用,返回对象本身。它和 apply 的区别在于:alsoitapplythis

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 的隐式调用。在 runapply 里,this 指向上下文对象,容易和外部作用域的 this 混淆。如果类里也有同名属性,会出问题。
  • 链式调用不要太长。超过4-5个链式调用,建议拆成多行,每行一个操作。

知识体系图

下面这张图帮你理清五个函数的关系和选择逻辑:

作用域函数选择指南 需要做什么? 配置对象并返回本身 转换/计算并返回结果 附加操作(日志等) apply let / run / with also 用 this 引用 返回对象本身 适合初始化配置 let: it引用, 非空判断 run: this引用, 配置+计算 with: this引用, 多次操作 用 it 引用 返回对象本身 适合日志/校验

实战建议

说了这么多,最后给你几条我个人的实战建议:

  • 团队统一风格。如果团队里有人习惯用 apply,有人用 also,代码会很难看。建议在代码规范里统一约定。
  • 优先用 apply 和 let。这两个函数覆盖了80%的场景。其他三个函数,等你真正需要时再用。
  • 不要为了用而用。如果一段代码用普通写法更清晰,就别强行套作用域函数。代码是给人读的,不是炫技的。
我的习惯:写 View 初始化用 apply,写网络请求参数用 also 打印日志,写数据转换用 let。这三个组合,基本能搞定日常开发。

好了,作用域函数就讲到这里。你想想看,是不是比之前清晰多了?下次写代码时,试着用这些函数重构一下,你会发现代码变得更简洁、更Kotlin了。


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