本文详细解析 Kotlin 标准库中的几个核心作用域函数(scope functions),包括 letapplyrunwithalsotakeIftakeUnless。通过对比它们的定义、参数、返回值和典型使用场景,帮助开发者更好地理解和运用这些函数。

博主博客

前提:Kotlin 的尾随闭包语法

Kotlin 支持尾随闭包(trailing lambda)语法:如果函数的最后一个参数是闭包,则该闭包可以写在括号外部;如果函数只有这一个参数,括号可以省略。

// 正常写法
button.setOnClickListener({
    Toast.makeText(context, "test", Toast.LENGTH_SHORT).show()
})

// 使用尾随闭包语法(推荐)
button.setOnClickListener {
    Toast.makeText(context, "test", Toast.LENGTH_SHORT).show()
}

本文介绍的所有函数都使用了这一语法特性。

各函数详解

1. repeat

定义:一个独立函数,非扩展函数。
作用:将指定操作重复执行指定次数。
闭包参数:当前迭代的索引(从0开始)。
返回值Unit

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}

示例

repeat(3) { index ->
    println("Hello world $index")
}
// 输出:
// Hello world 0
// Hello world 1
// Hello world 2

2. with

定义:一个独立函数,非扩展函数。
作用:将指定对象作为闭包的接收者(receiver)执行操作,并返回闭包的执行结果。
闭包参数:隐式接收者 this(可直接访问对象成员)。
返回值:闭包的最后一行结果。

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

示例

val list = with(ArrayList<String>()) {
    add("item1")
    add("item2")
    this // 返回列表本身
}
println(list) // 输出:[item1, item2]

3. let

定义:扩展函数。
作用:将调用对象作为闭包参数 it 执行操作,并返回闭包的执行结果。
闭包参数:调用对象本身作为 it
返回值:闭包的最后一行结果。

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

示例

val length = "Hello".let { 
    println("字符串是:$it")
    it.length // 返回长度
}
println("长度:$length")
// 输出:
// 字符串是:Hello
// 长度:5

4. apply

定义:扩展函数。
作用:将调用对象作为闭包接收者执行操作,返回对象本身
闭包参数:隐式接收者 this
返回值:调用对象本身(this)。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

示例

val list = ArrayList<String>().apply {
    add("apply1")
    add("apply2")
    println("初始化完成:$this")
} // list 就是刚刚初始化的 ArrayList
println("外部访问:$list")

5. run(扩展函数版本)

定义:扩展函数。
作用:将调用对象作为闭包接收者执行操作,并返回闭包的执行结果。
闭包参数:隐式接收者 this
返回值:闭包的最后一行结果。

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

示例

val result = "Hello".run {
    println("接收者是:$this")
    this.length // 返回长度
}
println("结果:$result")

6. run(非扩展函数版本)

定义:独立函数,非扩展函数。
作用:执行一个闭包,并返回其执行结果。
闭包参数:无参数。
返回值:闭包的最后一行结果。

public inline fun <R> run(block: () -> R): R {
    return block()
}

示例

val date = run {
    val now = System.currentTimeMillis()
    Date(now) // 返回新创建的 Date 对象
}
println("当前时间:$date")

7. also

定义:扩展函数(Kotlin 1.1+)。
作用:将调用对象作为闭包参数 it 执行操作,返回对象本身
闭包参数:调用对象本身作为 it
返回值:调用对象本身(this)。

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

示例

val list = ArrayList<String>().also {
    it.add("also1")
    it.add("also2")
    println("also 中间操作:$it")
} // list 就是刚刚修改的 ArrayList
println("最终结果:$list")

8. takeIf

定义:扩展函数(Kotlin 1.1+)。
作用:如果对象满足给定条件(闭包返回 true),则返回该对象,否则返回 null
闭包参数:调用对象本身作为 it
返回值:调用对象本身 或 null

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}

示例

val date = Date().takeIf {
    // 检查是否在 2018 年元旦之后
    it.after(Date(2018 - 1900, 0, 1))
}
println("符合条件的日期:$date") // 如果当前时间晚于 2018-01-01,则输出日期,否则输出 null

9. takeUnless

定义:扩展函数(Kotlin 1.1+)。
作用:与 takeIf 相反,如果对象满足给定条件(闭包返回 false),则返回该对象,否则返回 null
闭包参数:调用对象本身作为 it
返回值:调用对象本身 或 null

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    return if (!predicate(this)) this else null
}

示例

val date = Date().takeUnless {
    // 检查是否在 2018 年元旦之后
    it.after(Date(2018 - 1900, 0, 1))
}
println("不符合条件的日期:$date") // 如果当前时间早于 2018-01-01,则输出日期,否则输出 null

总结对比表

函数名 定义 闭包参数 闭包返回值 函数返回值 是否为扩展函数 特点
repeat repeat(times: Int, action: (Int) -> Unit) 索引值 Unit Unit 循环执行
with with(receiver: T, block: T.() -> R): R this(隐式) 任意类型 闭包返回值 非扩展,需显式传入对象
let T.let(block: (T) -> R): R it(显式) 任意类型 闭包返回值 常用于空安全链式调用
apply T.apply(block: T.() -> Unit): T this(隐式) Unit 对象本身(this) 对象初始化配置
run(扩展) T.run(block: T.() -> R): R this(隐式) 任意类型 闭包返回值 结合了 withlet 的特点
run(非扩展) run(block: () -> R): R 任意类型 闭包返回值 创建一个独立作用域
also T.also(block: (T) -> Unit): T it(显式) Unit 对象本身(this) 执行副作用,返回原对象
takeIf T.takeIf(predicate: (T) -> Boolean): T? it(显式) Boolean 对象本身 或 null 条件判断并返回原对象
takeUnless T.takeUnless(predicate: (T) -> Boolean): T? it(显式) Boolean 对象本身 或 null takeIf 的反向操作

如何选择合适的作用域函数?

根据你的需求,遵循以下决策路径:

  1. 是否需要返回对象本身?

    • → 选择 applyalso

      • 需要访问对象成员(使用 this)? → apply
      • 需要将对象作为参数(使用 it)? → also
    • → 进入步骤 2

  2. 是否需要将对象作为参数传递(使用 it)?

    • → 选择 lettakeIftakeUnless

      • 需要条件判断?
        • 条件为真时返回对象 → takeIf
        • 条件为假时返回对象 → takeUnless
      • 不需要条件判断 → let
    • → 进入步骤 3

  3. 是否需要将对象作为接收者(使用 this)?

    • → 选择 run(扩展版本)或 with

      • 对象可能为 null? → run
      • 对象不为 null? → with
    • → 选择 run(非扩展版本)(仅需执行代码块)

综合示例

示例 1:对象初始化

data class User(
    var id: Int = 0,
    var name: String? = null,
    var hobbies: List<String>? = null
)

// 1. 传统方式
val user1 = User()
user1.id = 1
user1.name = "张三"
user1.hobbies = listOf("篮球", "音乐")

// 2. 使用 apply(推荐:初始化配置)
val user2 = User().apply {
    id = 2
    name = "李四"
    hobbies = listOf("阅读", "旅行")
}

// 3. 使用 also(执行额外操作)
val user3 = User().also {
    it.id = 3
    it.name = "王五"
}.also {
    println("用户 ${it.name} 已创建")
}

// 4. 使用 let(转换数据)
val userId = User().let { user ->
    user.id = 4
    user.name = "赵六"
    user.id // 返回 id
}

示例 2:空安全与条件处理

data class Resp<T>(
    var code: Int = 0,
    var body: T? = null,
    var errorMessage: String? = null
) {
    fun isSuccess() = code == 200
}

// 传统 null 检查方式
fun processRespTraditional(resp: Resp<String>?) {
    if (resp != null) {
        if (resp.isSuccess()) {
            println("成功:${resp.body}")
        } else {
            println("失败:${resp.errorMessage}")
        }
    }
}

// 使用作用域函数的优雅方式
fun processRespModern(resp: Resp<String>?) {
    // 1. 使用 run(访问成员方便)
    resp?.run {
        if (isSuccess()) {
            println("成功:$body")
        } else {
            println("失败:$errorMessage")
        }
    }
    
    // 2. 使用 let(需要 it 参数)
    resp?.let {
        if (it.isSuccess()) {
            println("成功:${it.body}")
        } else {
            println("失败:${it.errorMessage}")
        }
    }
    
    // 3. 使用 takeIf 进行条件筛选
    resp?.takeIf { it.isSuccess() }?.let {
        println("只处理成功情况:${it.body}")
    }
    
    resp?.takeUnless { it.isSuccess() }?.let {
        println("只处理失败情况:${it.errorMessage}")
    }
}

示例 3:链式调用

// 链式调用多个作用域函数
val result = "hello world"
    .takeIf { it.length > 5 }        // 1. 长度大于5则保留字符串
    ?.let { it.toUpperCase() }        // 2. 转为大写
    ?.apply { println("中间值:$this") } // 3. 打印中间结果(返回原字符串)
    ?.run { this.split(" ") }         // 4. 分割为列表
    ?.also { println("最终列表:$it") }  // 5. 打印最终结果(返回原列表)

println(result) // 输出:["HELLO", "WORLD"]

实用技巧与注意事项

  1. 避免过度嵌套:链式调用虽然强大,但过度嵌套会降低可读性

    // 不推荐:嵌套过深
    user?.let { user ->
        user.name?.let { name ->
            name.takeIf { it.length > 3 }?.let { 
                println(it) 
            }
        }
    }
    
    // 推荐:使用多个链式调用
    user?.name
        ?.takeIf { it.length > 3 }
        ?.let { println(it) }
    
  2. 明确使用 it 还是 this

    • 当对象的主要目的是作为参数处理时,使用 itletalso
    • 当对象的主要目的是调用其成员时,使用 thisapplyrunwith
  3. 基于返回值选择函数

    • 需要继续操作原对象applyalso
    • 需要获取计算结果letrunwith
    • 需要条件过滤takeIftakeUnless
  4. 性能考虑:所有这些都是内联函数,运行时没有额外开销,可以放心使用。

结论

Kotlin 的作用域函数提供了强大的表达能力,能够让代码更加简洁、安全且富有表现力。掌握这些函数的区别和适用场景,能够显著提升 Kotlin 编程效率和代码质量。记住,选择哪个函数取决于你的具体需求:是否需要返回对象本身?是否需要将对象作为参数?理解了这一点,就能轻松选出最合适的工具。