Kotlin 作用域函数全解(run / with / apply / let / also + this/it 对比)

240 阅读6分钟

1 作用域函数概览

函数接收者类型Lambda 参数返回值典型用途
runT.() -> RthisLambda 返回值计算结果、对象配置后返回结果
withT.() -> RthisLambda 返回值已有对象批量操作
applyT.() -> Unitthis原对象(receiver)初始化、链式调用
let(T) -> RitLambda 返回值可空安全链、临时变量
also(T) -> Unitit原对象(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 可以省略,直接调用对象的方法或访问属性。
  • 在哪些作用域函数出现

    • runwithapply
  • 例子

    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 是默认参数名,可以手动改成别的名字。
  • 在哪些作用域函数出现

    • letalso
  • 例子

    复制编辑
    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 / applylet / 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返回值常用场景
runthis结果计算并返回结果
withthis结果批量调用已有对象
applythis对象初始化对象链式调用
letit结果可空链、变量作用域限制
alsoit对象调试、链式副作用

5 面试高频问题

一、语法类(1–8)

1) Kotlin 作用域函数有哪些?核心区别?

要点

  • run/with/apply/let/also
  • 维度① 接收者:this(run/with/apply) vs it(let/also)。
  • 维度② 返回:结果值(run/with/let) vs 接收者本身(apply/also)。

常见场景

  • 初始化/配置:apply
  • 可空链/变换:let
  • 插入副作用:also
  • 在对象上下文产出结果:run/with(已有对象批量成员用 with)。

易错点

  • 需要结果值却用了 apply/also(返回对象)。
  • let 里做副作用,导致返回值改变(应用 also)。

2) thisit 的区别?

要点

  • 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) runwith 区别?

要点

  • run扩展函数obj.run { … }
  • with顶层函数with(obj) { … }
  • 都返回结果值,语义近似。

建议

  • 统一团队风格;已有对象批量成员时用 with 可读性好。

4) applyalso 区别?

要点

  • 都返回接收者本身,用于不断链。
  • 上下文差异applythis(更像“配置对象”),alsoit(更像“插入副作用”)。

示例

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) letalso 都能拿到对象,返回值为什么不同?

要点

  • 设计定位不同:

    • 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) 结合 applyrun:配置 + 产出结果

示例

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 适合什么场景?

要点

  • 已有对象、需要批量成员调用返回值(例如 StringBuilderPaint 配置后要 toString()/bool)。

16) 同时用 letapply 做数据加工与初始化

示例

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) letit 与外部 it 冲突?

做法

  • 重命名参数:
list.map { it * 2 }.let { doubled -> println(doubled) }

19) with 不是扩展函数,为何能用 this

要点

  • with(receiver, block: receiver.() -> R)block 是接收者函数类型,块内 this 指向 receiver

20) runlet 都返结果,如何选?

要点

  • runthis 接收者,像在对象内部;
  • letit 形参,适合可空链/变换或避免 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)