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
)。
常见误区 & 注意事项
- 不要用
apply
去取计算结果 :apply
返回对象本身,不返回 lambda 的最后表达式。 with
不能用?.with
,如果对象可空,优先用?.run
或?.let
。this
vsit
:apply
/run
/with
的 lambda 是接收者 lambda(T.() -> R
),内部使用this
引用接收者(没有it
)。- 如果你习惯
it
(单参),那是let
/also
的风格。
- 嵌套作用域时要小心
this
冲突 ,可用标签(this@apply
、this@run
)或给外层变量命名:
kotlin
obj.apply {
other.apply {
println(this@apply) // 引用外层 obj
}
}
- 它们都是
inline
,因此几乎没有性能成本,可以放心使用。
选择建议(经验法则)
- 需要配置对象并返回对象 → 用
apply
(对象初始化); - 需要在对象上做事并返回一个计算值 → 用
run
(或with
); - 只是想把对象作为上下文语境写多行逻辑(不关心扩展/非扩展) → 用
with
(但多数场景run
更灵活); - 对象可空并想要安全调用 →
?.run { ... }
或?.let { ... }
(按是否需要this
或it
决定)。