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 决定)。
相关推荐
灰太狼大王灬19 分钟前
Go 项目从开发到部署笔记
开发语言·笔记·golang
小树懒(-_-)21 分钟前
SEO:Java项
java·开发语言
TeleostNaCl39 分钟前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
胖咕噜的稞达鸭1 小时前
二叉树进阶面试题:最小栈 栈的压入·弹出序列 二叉树层序遍历
开发语言·c++
wzg20161 小时前
pyqt5 简易入门教程
开发语言·数据库·qt
心静财富之门2 小时前
【无标题】标签单击事件
开发语言·php
草莓熊Lotso2 小时前
揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
开发语言·c++
小秋学嵌入式-不读研版2 小时前
C56-字符串拷贝函数strcpy与strnpy
c语言·开发语言·笔记
hui函数2 小时前
python全栈(基础篇)——day04:后端内容(字符编码+list与tuple+条件判断+实战演示+每日一题)
开发语言·数据结构·python·全栈
Never_Satisfied2 小时前
在JavaScript / HTML中,转移字符导致js生成的html出错
开发语言·javascript·html