主题
第 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 个维度的差异:
- 块内怎么引用对象:
it(参数) vsthis(接收者) - 函数返回什么: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经典用法:
- 判空 + 转换kotlin
val parsed: Int? = input?.toIntOrNull()?.let { it * 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"经典用法:
- 多个操作 + 算出最终结果kotlin
val msg = StringBuilder().run { append("Hello, ") append("world!") toString() // 返回最终字符串 } - 替代 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 五兄弟差别一句话总结?
答:
| 函数 | 块内引用 | 返回 |
|---|---|---|
let | it | lambda 结果 |
run | this | lambda 结果 |
with | this | lambda 结果(非扩展) |
apply | this | 对象本身 |
also | it | 对象本身 |
口诀:
- 想返回结果 →
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:块内itrun:块内this
kotlin
"hello".let { it.length } // 用 it
"hello".run { length } // 用 this,可省略 thisrun 在多次访问对象成员时更简洁;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,参数是itinline保证零运行时开销
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 误用最多。两种典型坑:
把
also当apply用(想配置但忘了 it)kotlinval u = User().also { name = "Alice" // ❌ 编译报错(也可能改了外层同名变量) }修复:用
apply或显式it.name = "Alice"apply里调用方法但忘了它返回对象kotlinval s = StringBuilder().apply { append("hi") toString() // ← 这一行的返回值被忽略了!s 是 StringBuilder,不是 String }修复:想要 String 用
run:kotlinval 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