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 决定)。
相关推荐
Tiger_shl2 小时前
【层面一】C#语言基础和核心语法-02(反射/委托/事件)
开发语言·c#
GW_Cheng2 小时前
分享一个vue2的tinymce配置
开发语言·javascript·ecmascript
路人与大师2 小时前
【Mermaid.js】从入门到精通:完美处理节点中的空格、括号和特殊字符
开发语言·javascript·信息可视化
你怎么知道我是队长2 小时前
C语言---循环结构
c语言·开发语言·算法
o0o_-_3 小时前
【go/gopls/mcp】官方gopls内置mcp server使用
开发语言·后端·golang
柿蒂3 小时前
从if-else和switch,聊聊“八股“的作用
android·java·kotlin
Dxy12393102163 小时前
python把文件从一个文件复制到另一个文件夹
开发语言·python
酷飞飞4 小时前
Qt Designer与事件处理
开发语言·qt·命令模式
天雪浪子4 小时前
Python入门教程之赋值运算符
开发语言·python