Skip to content

作用域函数 (Scope Functions)

源:Kotlin 作用域函数指南

作用域函数(let, run, with, apply, also)的核心作用是在对象的上下文中执行代码块。它们都是 内联函数 (Inline Functions),因此使用它们不会产生额外的运行时对象分配开销。

核心差异矩阵

选择作用域函数主要取决于两个维度:引用对象的方式 (this vs it)返回值

函数引用对象返回值典型实战场景
letit (默认)Lambda 结果空安全转换 (?.let)、引入局部变量作用域
runthisLambda 结果对象配置并计算结果、缩短命名空间
withthisLambda 结果对同一个对象执行多次操作 (非扩展函数)
applythis对象本身对象初始化 (Builder 模式)
alsoit (默认)对象本身链式调用中的副作用 (日志、验证)

实战代码详解

kotlin
// 1. let: 空安全转换
// 只有当 user 不为空时,才会执行块内代码,并返回最后一行的结果
val nameLength = user?.let {
    // it 指代 user
    println("Processing ${it.name}")
    it.name.length // 返回值
}

// 2. also: 附加操作
// 返回 list 本身,适合插入日志或断言
fun getList() = mutableListOf(1, 2, 3).also {
    println("List initialized with size: ${it.size}")
}
kotlin
// 3. apply: 配置对象
// 常用于 Android View 或 Intent 初始化
val intent = Intent(context, DetailActivity::class.java).apply {
    action = Intent.ACTION_VIEW
    putExtra("ID", 123)
    // 返回的是 Intent 本身
}

// 4. run: 配置并计算
// 结合了 with 的 this 引用和 let 的返回值特性
val status = StringBuilder().run {
    append("System: ")
    append("Online")
    toString() // 返回 String
}
kotlin
// 5. with: 结构化调用
// 适用于不需要返回结果,仅为了代码组织整洁
with(binding) {
    titleView.text = "Home"
    subtitleView.text = "Welcome"
    confirmButton.setOnClickListener { /*...*/ }
}

选型决策指南

当你在犹豫用哪个函数时,请参考此流程:

  • 需要返回对象本身吗?
    • 需要初始化/配置对象 (this) -> 用 apply
    • 需要做额外动作(如打日志)且不修改对象 (it) -> 用 also
  • 需要返回 Lambda 的计算结果吗?
    • 对象可空吗?
      • 是 (?.) -> 用 let
      • 否 -> 用 run
  • 对象已经存在且不需要返回值? -> 用 with

深度对比:apply vs also

虽然两者都返回对象本身,但语义侧重不同。

语义区别

  • apply (application): "将以下配置应用于该对象"。
  • also (additional): "并且做以下事情"。
kotlin
// Apply: 修改对象内部状态
val user = User().apply {
    name = "Virogu" // this.name
    age = 25
}

// Also: 使用对象进行外部交互
val file = File("data.txt").also {
    require(it.exists()) { "File must exist" }            // 验证
    Log.d("IO", "Opening file: ${it.absolutePath}") // 日志
}

变体:非扩展 run (Non-extension run)

除了 T.run,Kotlin 还提供了一个顶层的 run { ... } 函数。它不依赖任何接收者,仅仅是为了执行一个代码块并返回结果。

kotlin
val result = run {
    val x = 10
    val y = 20
    x + y // 返回 30
}

实战用途

  1. 构建初始化块:在脚本或对象初始化时,将相关逻辑归组,减少变量污染外部作用域。
  2. 表达式转化:将多个语句转化为一个表达式(因为 Kotlin 语法要求某些位置必须是表达式)。

辅助函数:takeIf 与 takeUnless

这两个函数用于在链式调用中进行条件过滤,极具函数式编程风格。

kotlin
// 满足条件则返回对象,否则返回 null
val validFile = selectedFile.takeIf { it.exists() && it.canRead() }
    ?: throw IOException("File not accessible")
kotlin
// 满足条件则返回 null (与 takeIf 相反)
val number = input.toIntOrNull()?.takeUnless { it < 0 } // 排除负数

最佳实践与反模式

避免嵌套地狱

尽量避免嵌套使用作用域函数,否则 itthis 的指向会变得极其混乱。如果必须嵌套,务必显式重命名参数

kotlin
// ❌ 糟糕:隐式 it 满天飞
user?.let {
    it.config?.let {
        println(it.name) // 这个 it 是谁?User 还是 Config?
    }
}

// ✅ 推荐:显式命名
user?.let { userObj ->
    userObj.config?.let { configObj ->
        println(configObj.name)
    }
}