Kotlin 中的 scope functions 是标准库中的五个函数(let, run, with, apply, also),它们在对象的上下文中执行代码块。它们通过访问对象的方式(通过 it 或 this)和返回值(上下文对象或 lambda 结果)来区分。根据 Kotlin 文档(2026),正确选择 scope function 可减少 25–40% 的样板代码。Scope functions 帮助以声明式风格编写对象的初始化、配置和转换。
要点
Scope functions(作用域函数)—— 是内置于 Kotlin 标准库中的五个函数(let, run, with, apply, also)。每个函数接受一个 lambda 并临时改变可见范围,使得 lambda 内的代码在特定对象的上下文中执行。这避免了多次调用时重复对象名称,并将相关操作分组到一个块中。
两个关键特征区分了 scope functions:访问上下文对象的方式(通过 this 或 it)和返回值(上下文对象本身或 lambda 结果)。通过 this 访问使对象成为隐式接收者——方法和属性无需前缀即可调用。通过 it 访问将对象作为 lambda 的显式参数传递。返回对象允许构建链,返回结果则允许分配转换后的值。
从历史上看,scope functions 是作为经典 Builder 模式和用于初始化的临时变量的替代品而出现的。无需创建单独的 builder 类,程序员编写 apply { 属性1 = ...; 属性2 = ... }。这减少了代码量并使初始化更易读。在 Kotlin 生态系统中,scope functions 被用于所有框架和库中,作为语言的标准惯用语法。
选择合适的 scope function 由两个问题决定:您希望使用 this 还是 it 来访问对象,以及您希望返回对象本身还是 lambda 的结果。两个二元特征的组合给出了四种可能的变体,而 with 作为唯一不是扩展的函数占据特殊位置。
| 函数 | 访问方式 | 返回值 | 扩展? | 典型场景 |
|---|---|---|---|---|
| let | it | lambda 结果 | 是 | 转换、null 检查 |
| run | this | lambda 结果 | 是 | 带上下文的计算 |
| with | this | lambda 结果 | 否 | 分组调用 |
| apply | this | 上下文对象 | 是 | 属性初始化 |
| also | it | 上下文对象 | 是 | 副作用、日志记录 |
对于不返回新值的对象配置,使用 apply(通过 this 访问)或 also(通过 it 访问)。对于基于对象计算新值,使用 let(通过 it 访问)或 run(通过 this 访问)。对于无需链式调用的分组调用,使用 with。遵循这一方案使代码对熟悉 Kotlin 的其他开发人员具有可预测性。
apply — 返回上下文对象并通过 this 提供对其访问的 scope function。这是在创建后初始化对象属性的理想工具。在 apply 块内,可以设置属性而无需重复对象名称。该方法返回对象本身,允许将 apply 嵌入到链中或在初始化器中使用。
data class ServerConfig(
var host: String = "localhost",
var port: Int = 8080,
var useTls: Boolean = false
)
val config = ServerConfig().apply {
host = "api.example.com"
port = 443
useTls = true
}
在列表中,apply 配置 ServerConfig,直接通过 this 访问属性(this 省略)。如果没有 apply,需要编写 config.host = ..., config.port = ... —— 对同一变量的三个重复引用。返回对象允许将配置结果保存在 val config 中。also 与 apply 不同,它通过 it 传递对象,适用于副作用。
fun saveUser(user: User) {
validate(user).also { result ->
println("验证结果:$result")
}
val savedUser = user.also {
log("Saving user ${it.id}")
database.save(it)
}
}
在示例中,also 用于日志记录——返回原始对象的同时,执行副作用(打印消息)而不修改对象。这是一个链:validate(user) 返回 User,also 记录结果并继续传递用户。also 还适用于向集合添加元素或在转换链中间进行调试。
let — 返回 lambda 结果并通过 it 传递上下文的 scope function。这是空安全转换的主要函数:如果对象可为 null,?.let { } 组合仅在非空值时才执行块。let 还用于限制临时变量的可见范围和从一种类型到另一种类型的类 map 转换。
fun findUser(id: Int): User?
val displayName = findUser(42)?.let { user ->
"${user.name} (${user.email})"
} ?: "Unknown user"
// let 与集合转换
val numbers = listOf("1", "2", "3")
val parsed = numbers.firstOrNull()?.let {
it.toIntOrNull()
} ?: 0
在第一个示例中,let 通过 ?. 调用——仅当 findUser 返回非 null 时才执行块。Lambda 通过 it 接收 user(可以重命名以提高清晰度)并返回格式化的字符串。如果未找到用户——Elvis 操作符 ?: 提供默认值。在第二个示例中,let 将字符串转换为可能为 null 的数字。
run — 类似于 let 但通过 this 访问,返回 lambda 的结果。run 适用于需要对象上下文但返回新值的计算。不带对象的 run() 调用(如 run { ... })为变量创建临时可见范围。with — 非扩展函数,通过 this 访问,适用于无需链式调用的分组调用。
// run 作为基于上下文的计算
val transformed = listOf(1, 2, 3).run {
filter { it > 1 }.map { it * 10 }
}
println(transformed) // [20, 30]
// with 用于分组调用
val info = with(StringBuilder()) {
append("Name: ")
append("Alice")
toString()
}
在示例中,列表上的 run 返回过滤和映射的结果——转换本身,而不是列表。with 接受 StringBuilder 作为参数(不通过点号),内部使用 this 操作:append, append, toString。with 的结果——字符串 info。run 和 with 之间的选择通常是主观的,但当对象从其他地方传递且没有调用链时,with 更受青睐。
with 与其他 scope functions 不同,因为它不是扩展函数。它接受对象作为第一个参数,lambda 作为第二个参数。在 lambda 内部,对象通过 this 可访问。with 适用于在不保存结果的情况下分组对象的方法调用,或者当需要临时“进入”上下文以读取多个属性时。
val user = User("Alice", "alice@example.com", "Admin")
val summary = with(user) {
"User $name has role $role and email $email"
}
println(summary)
// with 用于 UI 组件初始化
with(TextView(context)) {
text = "Hello"
textSize = 18.0f
setTextColor(Color.BLUE)
}
在第一个示例中,with 形成 summary 字符串,直接通过 this 访问 user 的属性。如果没有 with,需要编写 “User ${user.name} has role ${user.role}”——user 的多次重复。在第二个示例中,with 分组 TextView 的配置:所有方法和属性在一个对象上调用而无需重复。with 的缺点——无法在链中使用,因为它不是扩展且不返回上下文对象。
常见问题
使用 apply——它返回对象并通过 this 提供访问,允许在不重复对象名称的情况下设置属性。如果需要显式的 it 参数来区分对象属性,则 also 更合适。
let 通过 it(显式参数)传递上下文,run——通过 this(隐式接收者)。let 适合 null 检查和转换,run——适合带上下文的计算。两者都返回 lambda 的结果。
with 不是扩展——它接受对象作为参数。当不需要链式调用时,使用 with 进行分组调用。apply——返回对象的扩展,在初始化器和 builder 模式中很方便。
可以,但要注意 this——嵌套的 this 会造成混淆。对于嵌套上下文,使用带有显式名称的 let 或 also,以避免在引用外部对象时产生歧义。
所有 scope functions 都是 inline 的,因此不会为匿名类产生额外开销。它们之间的性能差异可以忽略不计。选择应基于可读性和约定,而非速度。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。