扩展、Lambda 与作用域函数

扩展函数、Lambda、高阶函数和作用域函数是 Kotlin 代码读起来与 Java 明显不同的主要原因。它们很强大,但也容易被滥用。本章重点讲清楚它们解决什么问题,以及什么时候应该克制使用。

扩展函数

扩展函数可以像给已有类型添加成员一样调用:

fun String.firstCharOrNull(): Char? =
    if (isEmpty()) null else this[0]

println("Kotlin".firstCharOrNull())

扩展函数并不会真的修改 String 类,也不能访问它的私有成员。它本质上是静态解析的函数调用,只是调用语法更自然。

Java 近似写法:

static Character firstCharOrNull(String value) {
    return value.isEmpty() ? null : value.charAt(0);
}

Kotlin 写成扩展后,调用方不需要记住工具类名。

扩展属性

val String.lastIndex: Int
    get() = length - 1

扩展属性不能有 backing field,因为它没有真正把字段加到目标类上。它只能通过 getter 或 setter 计算。

扩展的解析规则

扩展函数是静态解析,不是动态派发:

open class Shape
class Circle : Shape()

fun Shape.name() = "shape"
fun Circle.name() = "circle"

val shape: Shape = Circle()
println(shape.name()) // shape

调用哪个扩展由变量的静态类型决定,而不是运行时真实类型。不要用扩展函数模拟多态。

Lambda

val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(1, 2))

函数类型 (Int, Int) -> Int 表示接收两个 Int,返回 Int

集合操作中最常见:

val names = listOf("Ada", "Grace", "Linus")

val result = names
    .filter { it.length > 3 }
    .map { it.uppercase() }

单参数 Lambda 可以使用隐式参数 it。当逻辑变长或嵌套时,应显式命名参数。

高阶函数

接收函数作为参数的函数叫高阶函数:

fun retry(times: Int, action: () -> Unit) {
    repeat(times) {
        try {
            action()
            return
        } catch (ex: Exception) {
            if (it == times - 1) throw ex
        }
    }
}

调用:

retry(3) {
    println("执行可能失败的操作")
}

Java 8 之后可以用函数式接口实现类似效果,但 Kotlin 的函数类型和尾随 Lambda 让 DSL 和回调写法更简洁。

Lambda with receiver

带接收者的函数类型:

fun buildString(block: StringBuilder.() -> Unit): String {
    val builder = StringBuilder()
    builder.block()
    return builder.toString()
}

val text = buildString {
    append("Hello")
    append(", Kotlin")
}

StringBuilder.() -> Unit 表示 Lambda 内部的 thisStringBuilder。这类写法常用于 DSL,例如 Gradle Kotlin DSL。

作用域函数总览

Kotlin 标准库有五个常用作用域函数:

函数 上下文对象 返回值 常见用途
let it Lambda 结果 处理非空值、局部变量作用域
run this Lambda 结果 对象内计算结果
with this Lambda 结果 对同一对象执行一组操作
apply this 对象本身 配置对象
also it 对象本身 附加动作,例如日志

let

处理可空值:

val email: String? = findEmail()

email?.let {
    sendEmail(it)
}

当 Lambda 只有一个函数调用时,可以用函数引用:

email?.let(::sendEmail)

apply

配置对象:

val user = User().apply {
    name = "Ada"
    age = 36
}

apply 返回对象本身,因此适合对象初始化链。

also

附加动作:

val result = loadUsers()
    .also { println("loaded ${it.size} users") }
    .filter { it.active }

also 不改变链条中的对象,适合日志、调试、指标记录。

runwith

run 适合在对象上下文中计算一个结果:

val summary = user.run {
    "$name ($age)"
}

with 适合对同一对象执行一组操作:

with(builder) {
    append("Hello")
    append("Kotlin")
}

takeIftakeUnless

takeIf 满足条件返回对象,否则返回 null

val adult = user.takeIf { it.age >= 18 }

常与安全调用组合:

user
    .takeIf { it.active }
    ?.let { sendWelcomeMessage(it) }

滥用风险

作用域函数不会提供新的技术能力,只是改变代码组织。过度嵌套会降低可读性:

user?.let {
    it.address?.run {
        city.takeIf { it.isNotBlank() }?.also {
            println(it)
        }
    }
}

这种代码往往不如拆成清晰的局部变量和 if 判断。

实践建议

  • 扩展函数适合补充领域语言,不适合隐藏复杂副作用。
  • 不要用扩展函数模拟继承多态。
  • 简短 Lambda 可用 it,复杂 Lambda 应显式命名参数。
  • apply 用于配置对象,also 用于附加动作,let 常用于非空处理。
  • 避免嵌套多个作用域函数,尤其是同时混用 thisit

参考

  • 官方扩展文档:https://kotlinlang.org/docs/extensions.html
  • 官方 Lambda 文档:https://kotlinlang.org/docs/lambdas.html
  • 官方作用域函数文档:https://kotlinlang.org/docs/scope-functions.html