1 作用域函数概览
| 函数 | 接收者类型 | Lambda 参数 | 返回值 | 典型用途 |
|---|---|---|---|---|
run | T.() -> R | this | Lambda 返回值 | 计算结果、对象配置后返回结果 |
with | T.() -> R | this | Lambda 返回值 | 已有对象批量操作 |
apply | T.() -> Unit | this | 原对象(receiver) | 初始化、链式调用 |
let | (T) -> R | it | Lambda 返回值 | 可空安全链、临时变量 |
also | (T) -> Unit | it | 原对象(receiver) | 调试、日志、副作用 |
核心心智模型 — 两个维度
这五个标准库扩展函数,本质就是 作用域函数(Scope Functions) ,只有两个维度:
| 维度 | 取值 | 涉及函数 |
|---|---|---|
| 接收者引用方式 | this(隐式调用成员) | run / with / apply |
it(显式参数名) | let / also | |
| 返回值 | 结果值(最后一行表达式) | run / with / let |
| 接收者本身 | apply / also |
2 this vs it
this — 隐式接收者(Receiver)
-
是什么:
- 当作用域函数的参数是 接收者函数类型(
T.() -> R)时,函数体内的this代表调用该作用域函数的对象本身。 - 调用时
this可以省略,直接调用对象的方法或访问属性。
- 当作用域函数的参数是 接收者函数类型(
-
在哪些作用域函数出现:
run、with、apply
-
例子:
val str = "Hello" val length = str.run { // 这里 this = "Hello" println(this) // 输出 Hello length // this.length } println(length) // 输出 5 -
特点:
-
this是隐式的,不写也行:run { println(length) // 等价于 this.length }
-
it — 显式参数
-
是什么:
- 当作用域函数的参数是普通函数类型(
(T) -> R)时,调用对象会作为it变量传入 Lambda。 it是默认参数名,可以手动改成别的名字。
- 当作用域函数的参数是普通函数类型(
-
在哪些作用域函数出现:
let、also
-
例子:
复制编辑 val str = "Hello" val length = str.let { // 这里 it = "Hello" println(it) // 输出 Hello it.length } println(length) // 输出 5 -
自定义变量名:
str.let { value -> println(value) }
直观对比
| 特性 | this(隐式) | it(显式) |
|---|---|---|
| 是否可省略 | ✅ 可以省略 | ❌ 不可省略(除非改名) |
| 出现位置 | run / with / apply | let / also |
| 调用方式 | 直接访问属性或方法 | 通过 it 引用对象 |
| 场景 | 适合初始化和链式配置 | 适合可空安全链、类型转换、调试副作用 |
3 各函数详解与场景
run
- 原型:
fun <T, R> T.run(block: T.() -> R): R - 用途:执行一段逻辑并返回结果
- 示例:
val result = "Hello".run {
println(length)
length + 1
} // result = 6
with
- 原型:
fun <T, R> with(receiver: T, block: T.() -> R): R - 用途:批量调用已存在对象的方法
- 示例:
val sb = StringBuilder()
val text = with(sb) {
append("Hello ")
append("World")
toString()
}
apply
- 原型:
fun <T> T.apply(block: T.() -> Unit): T - 用途:配置对象并返回自身(链式调用)
- 示例:
val user = User("Tom", 20).apply {
name = "Jerry"
age = 25
}
let
- 原型:
fun <T, R> T.let(block: (T) -> R): R - 用途:可空安全调用、临时作用域
- 示例(可空链):
val length = nullableStr?.let {
println(it)
it.length
}
also
- 原型:
fun <T> T.also(block: (T) -> Unit): T - 用途:链式调用中插入副作用(调试、打印日志)
- 示例:
val list = mutableListOf(1, 2, 3).also {
println("Before: $it")
it.add(4)
}
4 对比速查表
| 函数 | this/it | 返回值 | 常用场景 |
|---|---|---|---|
| run | this | 结果 | 计算并返回结果 |
| with | this | 结果 | 批量调用已有对象 |
| apply | this | 对象 | 初始化对象链式调用 |
| let | it | 结果 | 可空链、变量作用域限制 |
| also | it | 对象 | 调试、链式副作用 |
5 面试高频问题
一、语法类(1–8)
1) Kotlin 作用域函数有哪些?核心区别?
要点
run/with/apply/let/also。- 维度① 接收者:
this(run/with/apply) vsit(let/also)。 - 维度② 返回:结果值(run/with/let) vs 接收者本身(apply/also)。
常见场景
- 初始化/配置:
apply; - 可空链/变换:
let; - 插入副作用:
also; - 在对象上下文产出结果:
run/with(已有对象批量成员用with)。
易错点
- 需要结果值却用了
apply/also(返回对象)。 - 在
let里做副作用,导致返回值改变(应用also)。
2) this 和 it 的区别?
要点
this:接收者函数类型T.() -> R,在run/with/apply;可省略,像在对象内部。it:普通函数类型(T) -> R,在let/also;显式参数名(可改名)。
示例
"Hi".run { length } // this.length
"Hi".let { it.length } // it = "Hi"
易错点
- 多层嵌套导致
this/it混淆 → 给it改名、或拆函数。
3) run 和 with 区别?
要点
run是扩展函数:obj.run { … }with是顶层函数:with(obj) { … }- 都返回结果值,语义近似。
建议
- 统一团队风格;已有对象批量成员时用
with可读性好。
4) apply 和 also 区别?
要点
- 都返回接收者本身,用于不断链。
- 上下文差异:
apply用this(更像“配置对象”),also用it(更像“插入副作用”)。
示例
val v = TextView(ctx).apply { text = "Hi" } // 配置
val user = repo.load().also { log(it) } // 副作用
易错点
- 在
also里做“语义性修改”会隐藏副作用 → 改用apply。
5) 为什么 let 常用于可空对象?
要点
?.let { … }仅在非空时执行,天然空安全;返回结果值,便于链式变换。
示例
val header = token?.let { "Bearer ${it.trim()}" }
易错点
let返回的是表达式结果,不是对象;只想不断链请用also。
6) let 与 also 都能拿到对象,返回值为什么不同?
要点
-
设计定位不同:
let(T)->R:变换/产值;also(T)->T:副作用/不断链。
示例
kotlin
复制编辑
val len = name.let { it.length } // R
val same = name.also { log(it) } // T
7) 顶层 run(无接收者)有什么用?
要点
- 创建小作用域并返回一个值,限制局部变量可见性,避免泄漏到外层。
示例
val path = run {
val base = filesDir
File(base, "config.json").absolutePath
}
8) 如何自定义类似 let 的作用域函数?
要点
- 作用域函数通常是泛型 + inline。
示例
inline fun <T, R> T.myLet(block: (T) -> R): R = block(this)
加分点
- 解释为什么
inline:减少创建Function对象与虚调度开销。
二、场景类(9–16)
9) 链式调用为何用 apply 而不是 let?
要点
- 链式需要返回对象继续链 →
apply/also。 let返回结果值,会中断对象链。
反例修正
// 反例
builder.let { it.setA(); it }
// 正解
builder.apply { setA() }
10) 对象初始化用 apply 的优势?
要点
- 上下文接收者是
this,无需重复对象名;返回对象本身,天然链式;表达“配置”的语义清晰。
11) 如何用 also 给链式加调试而不影响结果?
示例
val data = repo.fetch()
.also { Log.d("repo", "fetched $it") }
.map { transform(it) } // 链不被破坏
要点
also返回接收者,不改变链的数据流。
12) 用 let 优化可空判空
示例
token?.let { header("Authorization", "Bearer ${it.trim()}") }
对比
- if 判空冗长;
?.let{}简洁且局部作用域更安全。
13) 结合 apply 和 run:配置 + 产出结果
示例
val ok = Paint().apply {
isAntiAlias = true; style = Paint.Style.FILL
}.run { isAntiAlias && style == Paint.Style.FILL }
14) 用 run 做“局部同步块”(类比 synchronized)
示例
val result = lock.run {
synchronized(this) { compute() }
}
要点
run提供“在接收者上下文中产出值”的语义,包裹临界区更紧凑。
15) with 适合什么场景?
要点
- 已有对象、需要批量成员调用并返回值(例如
StringBuilder、Paint配置后要toString()/bool)。
16) 同时用 let 和 apply 做数据加工与初始化
示例
val req = Request.Builder().apply { url("…") }.also { log("building") }
val final = token?.let { "Bearer ${it.trim()}" }?.let { auth ->
req.apply { addHeader("Authorization", auth) }.build()
}
三、陷阱类(17–30)
17) apply 返回对象,链式后如何拿到中间计算结果?
做法
- 用局部变量 /
also捕获 / 组合run。
示例
var ok = false
val obj = Foo().apply { ok = check() } // also 也可
// 或
val ok2 = Foo().apply { init() }.run { check() }
18) let 的 it 与外部 it 冲突?
做法
- 重命名参数:
list.map { it * 2 }.let { doubled -> println(doubled) }
19) with 不是扩展函数,为何能用 this?
要点
with(receiver, block: receiver.() -> R)的 block 是接收者函数类型,块内this指向receiver。
20) run 和 let 都返结果,如何选?
要点
run:this接收者,像在对象内部;let:it形参,适合可空链/变换或避免this遮蔽。- 统一团队风格:对象上下文就用 run,普通变换用 let。
21) 作用域函数链式是否有性能损耗?
要点
- 标准库实现为
inline,通常无额外Function分配; - 但深链影响可读性;闭包捕获可变局部时会生成
Ref包装(轻微开销)。 - 热路径可用单循环/拆函数优化。
22) ?.let {} vs if (x != null) 取舍?
要点
- 性能几乎无差(都编译成等价结构);
?.let {}可读性更高、作用域更小;- 大块逻辑/多个分支:
if更直观。
23) 多层嵌套时如何区分 this/it?
做法
- 给
it改名;外层this用@label指定:
outer.apply {
inner.let { item ->
this@apply.doSomething(item)
}
}
24) Android UI 配置:apply vs with?
要点
- 初始化新 View:
apply(返 View 继续链)。 - 操作已有 View 并返回结果:
with(view) { … return value }。
25) 为什么 with 是顶层函数、apply 是扩展函数?
解释
-
语义层面:
with(obj){}强调“把这个对象拿来用一下”;obj.apply{}强调“在这个对象上做配置并把它还给我”。
-
设计上让表达更贴近语义。
26) Java 如何模拟作用域函数?
思路
- 使用 Builder/链式 API;或写工具方法传入 Consumer/Function:
static <T> T also(T t, Consumer<T> c){ c.accept(t); return t; }
static <T, R> R let(T t, Function<T, R> f){ return f.apply(t); }
27) 作用域函数的内联(inline)对性能影响?
要点
- 优点:避免创建
Function对象与虚调用; - 风险:内联过大函数→字节码膨胀;
- 建议:小而高频的工具函数标
inline,大型逻辑慎用。
28) run 可以替代 try-catch 吗?
两种写法
- 标准库
runCatching { … }(推荐):
val result = runCatching { risky() }.getOrElse { default() }
- 顶层
run {}只是作用域,不处理异常。
29) 在 Retrofit Builder 中用 also 做参数检查
示例
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl.also { require(it.startsWith("https")) })
.client(client.also { requireNotNull(it) })
.build()
要点
also插入校验不改变链;失败直接抛异常。
30) 作用域函数能和解构/解包配合吗?
可以
let/run返回结果后解构;或块内使用解构:
data class PairXY(val x:Int, val y:Int)
val (x, y) = getPoint().run { x to y } // or Pair(x, y)