第 8 章 · 作用域函数 (let / run / with / apply / also)

5 个看起来差不多的函数,搞懂区别就能写出"地道 Kotlin"代码。

📋 五兄弟对比表

函数 块内引用对象 返回值 典型场景
let it lambda 结果 可空对象判空 + 转换
x?.let { it.foo() }
run this lambda 结果 多操作 + 算结果(扩展函数)
x.run { foo(); bar() }
with this lambda 结果 多操作 + 算结果(普通函数)
with(x) { foo(); bar() }
apply this 对象本身 配置对象(设字段)
X().apply { name = "..." }
also it 对象本身 链中加日志 / 校验 / 副作用
x.also { log(it) }

💡 记忆口诀

想要返回结果(计算/转换)→ let / run / with
想要返回对象本身(链式/配置)→ apply / also

块内用 it(参数式)→ let / also
块内用 this(接收者式)→ run / with / apply

🔬 实现原理(都是 inline 函数)

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

public inline fun <T, R> T.run(block: T.() -> R): R = block()

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

🌳 决策树:根据需求选对函数

Q1: 你需要返回什么?
回答 Q1 → Q2 看推荐

🎯 常见场景速查

场景选哪个
判空后做点事再返回结果?.let { }
配置一个新对象(设 N 个字段).apply { }
链中加 println 调试.also { println(it) }
多操作然后算 toString.run { ... }
非空对象 + 多次访问其成员with(x) { ... }
临时变量不污染外层作用域.let { tmp -> ... }

🔬 同一需求:用 5 个函数分别写

需求:"给一个 user 对象做点事,最后打印它的 name"。注意每个写法的差异。

let

val r = user.let {
  println(it.name)
  it.name.length
}
→ r = 5 (Int)

run

val r = user.run {
  println(name)
  name.length
}
→ r = 5 (Int)

with

val r = with(user) {
  println(name)
  name.length
}
→ r = 5 (Int)

apply

val r = user.apply {
  println(name)
  name.length
  // ↑ 这行被丢弃
}
→ r = User(...) (User 对象)

also

val r = user.also {
  println(it.name)
  it.name.length
  // ↑ 这行被丢弃
}
→ r = User(...) (User 对象)

📊 总结

  • let / run / with:lambda 最后一行是返回值,所以你能拿到 length
  • apply / also:返回的是原对象,length 被丢了
  • this(run / with / apply)能省略 it.,但嵌套时容易混淆
  • it(let / also)显式但安全,链式特别清楚

🚀 实战:HTTP 请求构建(4 种风格对比)

同一个需求:构建一个 HTTP 请求,配置参数,发送,处理响应,打印结果。

① 没有作用域函数(一堆赋值)

val request = HttpRequest()
request.url = "https://api.example.com/users"
request.method = "POST"
request.body = """{"name":"Alice"}"""
request.headers["Content-Type"] = "application/json"
println("准备: $request")
val response = request.execute()
val parsed = parseJson(response)
println("解析: $parsed")

② 用 apply 配置(清爽 1 个数量级)

val request = HttpRequest().apply {
    url = "https://api.example.com/users"
    method = "POST"
    body = """{"name":"Alice"}"""
    headers["Content-Type"] = "application/json"
}
println("准备: $request")
val response = request.execute()
val parsed = parseJson(response)
println("解析: $parsed")

③ 用链式 apply + also + run(最现代)

val parsed = HttpRequest().apply {
    url = "https://api.example.com/users"
    method = "POST"
    body = """{"name":"Alice"}"""
    headers["Content-Type"] = "application/json"
}.also { println("准备: $it") }
 .execute()
 .run { parseJson(this) }
 .also { println("解析: $it") }

④ 数据类 + 命名参数(如果对象是不可变的)

data class HttpRequest(
    val url: String,
    val method: String = "GET",
    val body: String? = null,
    val headers: Map<String, String> = emptyMap(),
)

val parsed = HttpRequest(
    url = "https://api.example.com/users",
    method = "POST",
    body = """{"name":"Alice"}""",
    headers = mapOf("Content-Type" to "application/json"),
).also { println("准备: $it") }
 .execute()
 .let(::parseJson)
 .also { println("解析: $it") }
💡 选择建议
- 对象有很多 var 字段需要设置 → 方案 ②
- 想做完整流水线(构建 → 发送 → 处理)→ 方案 ③
- 对象设计成不可变 → 方案 ④(最 Kotlin 风格)

🧪 实时尝试

下面是一个简化的"输入 → 转换 → 输出"实验台。

📤 输出

点 ▶ 模拟运行