Kotlin 的 apply / with / run 详解

Kotlin 的 apply / with / run 详解

​ Kotlin存在三个作用域函数,啥意思呢?我们知道我们创建一个复杂对象的时候,往往需要调用相应的接口进行复杂的设置,举个例子

复制代码
    val lists = listOf("Apple", "Banana", "Charlies")
    val stringBuilder = StringBuilder()
    stringBuilder.append("About to Enumerate the sessions")
    for(each in lists){
        stringBuilder.append(each).append('\n')
    }
    stringBuilder.append("Enumerate OK")
    val result = stringBuilder.toString()
    println("OK Lets see the result $result")

​ 以最为常见的列表转可读的为例子,您可以看到,我们书写了大量累人的stringBuilder,Kotlin的设计者发现了这一点,包括和采纳其他现代语言,支持了更加现代的写法,这就是咱们今天的主角:Apply, Run和With。笔者目前认为,他们的区别没有非常大,在一些情况下可以互换。

函数 调用方式 lambda 的接收者 lambda 可用的引用 返回值(R就是结果的意思) 典型用途
apply 扩展函数 T.() -> Unit this T(接收者) 对对象做配置/初始化(构造后立即设置)
run 扩展函数 或 顶层 T.() -> R() -> R this(扩展)或无接收者(顶层) R(lambda 的结果) 在对象上执行并返回计算结果 ;或用顶层 run 做局部作用域/计算
with 顶层函数 with(receiver, block) T.() -> R this R 在不想写 .run/扩展形式时用于对某对象执行多次操作并返回值 (语义上和 run 很像)

备注:这三者均为 inline 函数(没有额外运行时开销)。

kotlin 复制代码
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <R> run(block: () -> R): R        // 顶层 run(无接收者)
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

apply

​ Apply本身表达的是应用,对此,我们就知道Apply用在我们懒得写Builder的时候才会使用。这个时候,我们在Apply的Function Block中写好我们对对象的配置,然后默认的时候就会返回回来。举个例子,咱们现在存在一个纯粹的数据类(C++中咱们可以理解为平凡的POD结构体):

kotlin 复制代码
data class Person(var name: String = "", var age: Int = 0)

// 初始化对象
val p = Person().apply {
    name = "张三"
    age = 30
}
// p: Person(name="张三", age=30)

apply 的 lambda 最终值被忽略(因为 lambda 类型是 Unit),如果你想得到一个计算结果,别用 apply。咱们应该使用其他的。当然,我们同样支持对一个潜在空的对象进行操作:

kotlin 复制代码
val p: Person? = ...
p?.apply { age = 31 }   // 返回 Person?,当 p 为 null 时结果为 null

run

​ run有两种形式,一种是对对象自身的run,还有一种是纯粹的带返回值的匿名函数形式。在形式上就区分出来了两种:扩展形式 T.run { ... }(lambda 的接收者是 T)和顶层形式 run { ... }(无接收者)。他们返回的就是最终我们计划返回的值。

  • 用法一(在对象上执行并返回计算结果):
kotlin 复制代码
val p = Person("李四", 20)
val isAdult: Boolean = p.run { age >= 18 }  // 返回 Boolean
  • 用法二(顶层 run,用作局部作用域并返回值):
kotlin 复制代码
val result = run {
    val tmp = heavyCalc()
    tmp * 2
}
  • Null-safe:
kotlin 复制代码
val length: Int? = pNullable?.run { name.length }  // 如果 pNullable 为 null,结果为 null

with(只能用在一定非空的对象是)

​ with有点像Python的with,实际上是针对给定的对象进行操作,写法是 with(obj) { ... },lambda 接收者是 obj。语义上等同于 obj.run { ... },但写法不同(with 把接收者作为第一个参数传入),最后我们就会得到返回 lambda 的最后表达式(R)。

kotlin 复制代码
val summary = with(p) {
    // this 引用 p
    "$name is $age years old"
}

区别点 :因为 with 不是扩展,所以不能直接用安全调用操作符(?.with 不存在)。如果对象可能为 null,通常写:

kotlin 复制代码
pNullable?.let { with(it) { /*...*/ } }  // 或直接用 pNullable?.run { ... }

​ 因此很多人更倾向于用 run(扩展形式)代替 with,因为 run 更灵活(支持 ?.run)。


常见误区 & 注意事项

  1. 不要用 apply 去取计算结果apply 返回对象本身,不返回 lambda 的最后表达式。
  2. with 不能用 ?.with ,如果对象可空,优先用 ?.run?.let
  3. this vs it
    • apply / run / with 的 lambda 是接收者 lambda(T.() -> R),内部使用 this 引用接收者(没有 it)。
    • 如果你习惯 it(单参),那是 let / also 的风格。
  4. 嵌套作用域时要小心 this 冲突 ,可用标签(this@applythis@run)或给外层变量命名:
kotlin 复制代码
obj.apply {
    other.apply {
        println(this@apply) // 引用外层 obj
    }
}
  1. 它们都是 inline,因此几乎没有性能成本,可以放心使用。

选择建议(经验法则)

  • 需要配置对象并返回对象 → 用 apply(对象初始化);
  • 需要在对象上做事并返回一个计算值 → 用 run(或 with);
  • 只是想把对象作为上下文语境写多行逻辑(不关心扩展/非扩展) → 用 with(但多数场景 run 更灵活);
  • 对象可空并想要安全调用 → ?.run { ... }?.let { ... }(按是否需要 thisit 决定)。
相关推荐
Kapaseker15 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish1 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker2 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker3 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z4 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton5 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream5 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam5 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker6 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
郑州光合科技余经理6 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php