Skip to content

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

学习目标:彻底搞懂"五兄弟"的差别,能在合适的场景选对函数;写出"地道 Kotlin"代码;摆脱"什么时候用 let 什么时候用 apply"的困惑。


8.1 五兄弟概览

Kotlin 标准库有 5 个非常常用的"作用域函数",它们都接受一个 lambda,在某个对象的作用域内执行代码

                 ★ 五兄弟全家福 ★

   函数        块内引用对象        返回值          典型场景
  ──────     ──────────────    ──────────       ──────────
   let        it             lambda 结果      可空判空 + 转换
   run        this           lambda 结果      多操作 + 算结果
   with       this           lambda 结果      多操作 + 算结果(不是扩展)
   apply      this           对象本身         配置对象
   also       it             对象本身         附带操作(log、check)

只有 2 个维度的差异:

  1. 块内怎么引用对象it(参数) vs this(接收者)
  2. 函数返回什么:lambda 结果 vs 对象本身

8.2 五兄弟逐一拆解

8.2.1 let —— 块内 it,返回 lambda 结果

kotlin
val name: String? = "Alice"
val length = name?.let {
    println("got name: $it")
    it.length        // ← 块的最后一行是返回值
}
println(length)      // 5

经典用法

  1. 判空 + 转换
    kotlin
    val parsed: Int? = input?.toIntOrNull()?.let { it * 2 }
  2. 作用域临时变量(不污染外层)
    kotlin
    computeExpensive().let { result ->
        println(result)
        saveToDb(result)
    }

8.2.2 run —— 块内 this,返回 lambda 结果

kotlin
val result = "Alice".run {
    println("length: $length")     // 直接用 length,不用 it.length
    uppercase()
}
println(result)      // "ALICE"

经典用法

  1. 多个操作 + 算出最终结果
    kotlin
    val msg = StringBuilder().run {
        append("Hello, ")
        append("world!")
        toString()        // 返回最终字符串
    }
  2. 替代 if-let 链
    kotlin
    val firstName = user?.run { name?.split(" ")?.first() }

8.2.3 with —— 块内 this,返回 lambda 结果(不是扩展)

kotlin
val sb = StringBuilder()
val result = with(sb) {
    append("Hello, ")
    append("world!")
    toString()
}

run 的唯一区别run 是扩展函数(obj.run { }),with 是普通函数(with(obj) { })。

💡 何时用 with:传给 with 的对象保证非空,避免 ?.run { } 嵌套。

8.2.4 apply —— 块内 this,返回对象本身

kotlin
val user = User().apply {
    name = "Alice"           // 直接当 this.name 用
    age = 25
}
// apply 返回的是 user 自己(链式调用很方便)

经典用法 1:配置对象(替代 Builder 模式)

kotlin
val intent = Intent(this, MainActivity::class.java).apply {
    putExtra("name", "Alice")
    putExtra("age", 25)
    flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
}

经典用法 2:链式构建

kotlin
val request = HttpRequest().apply {
    url = "https://api.example.com"
    method = "POST"
    headers["Content-Type"] = "application/json"
}.execute()

8.2.5 also —— 块内 it,返回对象本身

kotlin
val list = mutableListOf(1, 2, 3).also {
    println("初始 list: $it")
}.apply {
    add(4)
}.also {
    println("加完后: $it")
}

经典用法:在链中插入"附带操作",不影响主流程

  • 打日志
  • 校验
  • 触发副作用
kotlin
val parsed = input
    .also { require(it.isNotBlank()) { "输入不能为空" } }   // 校验
    .trim()
    .also { println("trimmed: $it") }                        // 日志
    .toInt()

8.3 决策树:怎么选?

                     需要在对象上做点事

                ┌───────────┴───────────┐
                │                       │
        想返回对象本身              想返回结果(不是对象)
        (链式 / 配置)              (转换 / 计算)
                │                       │
        ┌───────┼───────┐       ┌───────┼───────┐
        │               │       │               │
    用 this         用 it    用 this         用 it
        │               │       │               │
        ▼               ▼       ▼               ▼
      apply          also    run / with        let
   (配置 / 构建)  (日志 / 校验) (多操作 + 结果) (判空 / 转换)

一张表速查

场景用哪个
可空对象 + 想转换?.let { ... }
配置对象(设字段)apply { ... }
链式中插入日志/校验also { ... }
多个操作 + 算最终结果run { ... }with(...) { }
临时变量不想污染外层let { }

8.4 反模式:别这么写

8.4.1 嵌套 let / 嵌套 apply

kotlin
// ❌ 难看
user.let { u ->
    u.address.let { a ->
        a.city.let { c -> println(c) }
    }
}

// ✅ 链式 ?.
println(user.address.city)

8.4.2 用 also 取代 println

kotlin
// ❌ 没必要
val x = compute().also { println(it) }

// ✅ 直接写
val x = compute()
println(x)

also { println(it) } 只在链中才有意义,单行场景没收益。

8.4.3 apply 当成"赋值简写"

kotlin
// ❌ 滥用
val name = "Alice".apply {
    println(this)
}

// 这里 apply 返回的是字符串本身,跟下面没区别:
val name = "Alice"
println(name)

8.5 实战:HTTP 请求构建器

没有作用域函数的写法

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

用作用域函数

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

差不多紧凑了 50%,意图也更清晰。


8.6 章末小结

              ★ 第 8 章核心知识图谱 ★

        ┌──────────────┼──────────────┐
        │              │              │
    ┌──▼──┐        ┌──▼──┐        ┌──▼──┐
    │ it  │        │this │        │ 返回 │
    ├─────┤        ├─────┤        ├─────┤
    │let  │        │run  │        │lambda│
    │also │        │with │        │结果  │
    │     │        │apply│        │ vs  │
    │     │        │     │        │对象  │
    └─────┘        └─────┘        └─────┘

🎤 8.7 章末面试题(10 道)

Q1. let / run / with / apply / also 五兄弟差别一句话总结?

函数块内引用返回
letitlambda 结果
runthislambda 结果
withthislambda 结果(非扩展)
applythis对象本身
alsoit对象本身

口诀:

  • 想返回结果let / run / with
  • 想返回对象apply / also
  • this 简洁但易引用混乱 → 简单场景用
  • it 显式但安全 → 链式 + 多对象嵌套用

Q2. apply 和 also 看起来差不多,区别在哪?

  • apply:块内 this,适合改对象状态
  • also:块内 it,适合做副作用但不改主对象
kotlin
// apply:写字段更顺
val user = User().apply {
    name = "Alice"        // this.name = "Alice"
    age = 25
}

// also:在链里加日志 / 校验
val parsed = input
    .also { require(it.isNotBlank()) }       // 校验
    .also { println("got: $it") }            // 日志
    .toInt()

Q3. let 和 run 区别?

  • let:块内 it
  • run:块内 this
kotlin
"hello".let { it.length }       // 用 it
"hello".run { length }          // 用 this,可省略 this

run 在多次访问对象成员时更简洁let 在 lambda 嵌套时更安全(外层 it 不被覆盖)。


Q4. with 和 run 啥区别?

  • run扩展函数obj.run { }
  • with普通函数with(obj) { }

行为完全一样。run 支持安全调用 obj?.run { }with 不行(需要先判空)。

kotlin
val r1 = obj.run { ... }           // 等价
val r2 = with(obj) { ... }         // 等价

obj?.run { ... }                   // ✅ 可空安全
with(obj) { ... }                  // ❌ obj 必须非空

Q5. 什么时候用 ?.let { } 而不是 if (x != null) { }?

  • 链式场景user?.address?.city?.let { ... },比 if 嵌套清爽
  • 作为表达式返回值val name = user?.let { it.name } ?: "(无)"
  • var 或开放属性(smart cast 失效):obj.x?.let { ... } 块内 it 保证非空

简单的单层判空,用 if 也可以。


Q6. apply 配置对象 vs Java Builder 模式哪个好?

:在 Kotlin 里 apply + data class 更优雅

kotlin
// Java Builder
HttpRequest req = new HttpRequest.Builder()
    .url("...")
    .method("POST")
    .build();

// Kotlin apply
val req = HttpRequest().apply {
    url = "..."
    method = "POST"
}

// Kotlin 数据类 + 命名参数(更推荐)
val req = HttpRequest(url = "...", method = "POST")

如果对象有 5+ 字段且大部分有默认值,直接用命名参数 + 默认值最清爽。


Q7. let / run / apply 在源码里是怎么实现的?

:都是 inline 函数,几行就实现了:

kotlin
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
}

注意:

  • T.() -> R 是带 receiver 的 lambda,块内 this 就是接收者
  • (T) -> R 是普通 lambda,参数是 it
  • inline 保证零运行时开销

Q8. 嵌套 let 块里的 it 会冲突吗?

:会被遮蔽,建议显式命名:

kotlin
user?.let { u ->                          // 显式命名为 u
    u.address?.let { a ->                  // 命名为 a
        println("${u.name} in ${a.city}")  // 都能引用
    }
}

如果都用 it,内层会遮蔽外层,写起来容易乱。


Q9. 作用域函数会有性能开销吗?

没有。所有 5 个都是 inline 函数,编译后 lambda 直接展开,字节码跟手写 if/if 嵌套等价。所以放心用。


Q10. 最容易踩坑的是哪个?怎么避免?

apply 误用最多。两种典型坑:

  1. alsoapply(想配置但忘了 it)

    kotlin
    val u = User().also {
        name = "Alice"   // ❌ 编译报错(也可能改了外层同名变量)
    }

    修复:用 apply 或显式 it.name = "Alice"

  2. apply 里调用方法但忘了它返回对象

    kotlin
    val s = StringBuilder().apply {
        append("hi")
        toString()     // ← 这一行的返回值被忽略了!s 是 StringBuilder,不是 String
    }

    修复:想要 String 用 run

    kotlin
    val s = StringBuilder().run {
        append("hi")
        toString()     // ✅ run 返回这个表达式
    }

下一章 → 第 9 章 · 协程基础 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 8 章 · 作用域函数 — 综合示例
 *
 * 五兄弟:let / run / with / apply / also
 *
 *   函数        块内引用    返回         典型场景
 *   ────       ──────     ────         ──────
 *   let        it         lambda 结果   可空判空 + 转换
 *   run        this       lambda 结果   多操作 + 算结果
 *   with       this       lambda 结果   多操作 + 算结果(非扩展)
 *   apply      this       对象本身      配置对象
 *   also       it         对象本身      日志 / 校验 / 副作用
 */

class HttpRequest {
    var url: String = ""
    var method: String = "GET"
    var body: String? = null
    val headers = mutableMapOf<String, String>()
    override fun toString() = "$method $url, headers=$headers, body=$body"
    fun execute() = "{\"status\":\"ok\",\"data\":\"hello\"}"
}

fun main() {
    println("=== 1. let:可空判空 + 转换 ===")
    val name: String? = "Alice"
    val len = name?.let {
        println("  name 不为空,长度是 ${it.length}")
        it.length * 2
    }
    println("  len = $len")

    val nullName: String? = null
    val len2 = nullName?.let { it.length } ?: -1
    println("  null 时返回默认: $len2")

    println("\n=== 2. run:多操作 + 算结果 ===")
    val msg = StringBuilder().run {
        append("Hello, ")
        append("Kotlin!")
        toString()                        // 返回最后一行
    }
    println("  msg = $msg")

    val firstChar = "Alice".run {
        println("  长度: $length")
        println("  大写: ${uppercase()}")
        first()                            // 返回 'A'
    }
    println("  firstChar = $firstChar")

    println("\n=== 3. with:跟 run 一样,但不是扩展 ===")
    val sb = StringBuilder()
    val s = with(sb) {
        append("a"); append("b"); append("c")
        toString()
    }
    println("  s = $s")

    println("\n=== 4. apply:配置对象(最常用!)===")
    val request = HttpRequest().apply {
        url = "https://api.example.com/users"
        method = "POST"
        body = """{"name":"Alice"}"""
        headers["Content-Type"] = "application/json"
        headers["Authorization"] = "Bearer xxx"
    }
    println("  $request")

    println("\n=== 5. also:链中插入副作用 ===")
    val parsed = "  42  "
        .also { println("  原始: '$it'") }
        .trim()
        .also { println("  trim 后: '$it'") }
        .toInt()
        .also { println("  转 Int: $it") }
    println("  最终 parsed = $parsed")

    println("\n=== 6. 链式组合:apply + also + run ===")
    val response = HttpRequest().apply {
        url = "https://api.example.com/v2/users"
        method = "GET"
    }.also {
        println("  📤 准备发送: $it")
    }.execute().run {
        println("  📥 收到: $this")
        // 解析 JSON 取 data 字段(简单提取)
        substringAfter("\"data\":\"").substringBefore("\"")
    }.also {
        println("  ✅ 解析: $it")
    }
    println("  response = $response")

    println("\n=== 7. 五兄弟同需求对比 ===")
    val data = "Alice"

    val r1 = data.let { it.length }
    val r2 = data.run { length }
    val r3 = with(data) { length }
    val r4 = data.apply { /* 没法返回 length,因为返回的是 data 自己 */ }
    val r5 = data.also { /* 同上 */ }

    println("  let:   返回 $r1 (Int)")
    println("  run:   返回 $r2 (Int)")
    println("  with:  返回 $r3 (Int)")
    println("  apply: 返回 \"$r4\" (原 String,length 被丢弃)")
    println("  also:  返回 \"$r5\" (原 String)")

    println("\n=== 8. 反模式示例 ===")
    val x = compute().also { println("  got: $it") }       // ✅ 可读
    println("  x = $x")

    // ❌ 反模式:单行用 also 反而绕
    val y = compute()
    println("  got: $y")
    println("  y = $y")
}

fun compute() = 42

ScopeFunctionDemo.kt ↗