扩展、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 内部的 this 是 StringBuilder。这类写法常用于 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 不改变链条中的对象,适合日志、调试、指标记录。
run 与 with¶
run 适合在对象上下文中计算一个结果:
val summary = user.run {
"$name ($age)"
}
with 适合对同一对象执行一组操作:
with(builder) {
append("Hello")
append("Kotlin")
}
takeIf 与 takeUnless¶
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常用于非空处理。- 避免嵌套多个作用域函数,尤其是同时混用
this和it。
参考¶
- 官方扩展文档:https://kotlinlang.org/docs/extensions.html
- 官方 Lambda 文档:https://kotlinlang.org/docs/lambdas.html
- 官方作用域函数文档:https://kotlinlang.org/docs/scope-functions.html