扩展 Lambda
扩展 Lambda 类似于扩展函数。它定义了一个 lambda,而不是一个函数。
在这里,va
和 vb
产生相同的结果:
// ExtensionLambdas/Vanbo.kt
package extensionlambdas
import atomictest.eq
val va: (String, Int) -> String = { str, n ->
str.repeat(n) + str.repeat(n)
}
val vb: String.(Int) -> String = {
this.repeat(it) + repeat(it)
}
fun main() {
va("Vanbo", 2) eq "VanboVanboVanboVanbo"
"Vanbo".vb(2) eq "VanboVanboVanboVanbo"
vb("Vanbo", 2) eq "VanboVanboVanboVanbo"
// "Vanbo".va(2) // 不能编译通过
}
va
是一个普通的 lambda,就像在本书中看到的那些一样。它接受两个参数,一个 String
和一个 Int
,并返回一个 String
。lambda 主体也有两个参数,后面跟随必需的箭头:str, n ->
。
vb
将 String
参数移到括号外,并使用扩展函数语法:String.(Int)
。就像 扩展函数 一样,被扩展类型的对象(在这种情况下是 String
)成为 接收者,可以使用 this
访问它。
在 vb
中的第一个调用使用了显式形式 this.repeat(it)
。第二个调用省略了 this
,得到了 repeat(it)
。与任何 lambda 一样,如果只有一个参数(在这种情况下是 Int
),it
将引用该参数。
在 main()
中,对 va()
的调用正是您从 lambda 类型声明 (String, Int) -> String
中所期望的——在传统函数调用中有两个参数。vb()
是一个扩展,因此可以使用扩展形式 "Vanbo".vb(2)
进行调用。vb()
也可以使用传统形式 vb("Vanbo", 2)
进行调用。va()
无法使用扩展形式进行调用。
当您首次看到扩展 lambda 时,似乎 String.(Int)
部分是您应该关注的。但是,String
并不是通过参数列表 (Int)
进行扩展的——它是通过整个 lambda 进行扩展的:String.(Int) -> String
Kotlin 文档通常将扩展 lambda 称为 带接收者的函数字面值。术语 函数字面值 包括 lambda 和匿名函数。术语 带接收者的 lambda 经常与 扩展 lambda 同义使用,以强调它是带有附加隐式参数的 lambda。
与扩展函数一样,扩展 lambda 可以有多个参数:
// ExtensionLambdas/Parameters.kt
package extensionlambdas
import atomictest.eq
val zero: Int.() -> Boolean = {
this == 0
}
val one: Int.(Int) -> Boolean = {
this % it == 0
}
val two: Int.(Int, Int) -> Boolean = {
arg1, arg2 ->
this % (arg1 + arg2) == 0
}
val three: Int.(Int, Int, Int) -> Boolean = {
arg1, arg2, arg3 ->
this % (arg1 + arg2 + arg3) == 0
}
fun main() {
0.zero() eq true
10.one(10) eq true
20.two(10, 10) eq true
30.three(10, 10, 10) eq true
}
在 one()
中,使用 it
替代了参数的命名。如果这产生了不清晰的语法,最好使用显式的参数名。
我们一直通过定义 val
来演示扩展 lambda,但它们通常以函数参数的形式出现,就像 f2()
中的例子:
// ExtensionLambdas/FunctionParameters.kt
package extensionlambdas
class A {
fun af() = 1
}
class B {
fun bf() = 2
}
fun f1(lambda: (A, B) -> Int) =
lambda(A(), B())
fun f2(lambda: A.(B) -> Int) =
A().lambda(B())
fun lambdas() {
f1 { aa, bb -> aa.af() + bb.bf() }
f2 { af() + it.bf() }
}
在 main()
中,注意在传递给 f2()
的 lambda 中更简洁的语法。
如果扩展 lambda 返回 Unit
,则忽略 lambda 主体产生的结果:
// ExtensionLambdas/LambdaUnitReturn.kt
package extensionlambdas
fun unitReturn(lambda: A.() -> Unit) =
A().lambda()
fun nonUnitReturn(lambda: A.() -> String) =
A().lambda()
fun lambdaUnitReturn () {
unitReturn {
"Unit ignores the return value" +
"So it can be anything ..."
}
unitReturn { 1 } // ... of any type ...
unitReturn { } // ... or nothing
nonUnitReturn {
"Must return the proper type"
}
// nonUnitReturn { } // 不可行
}
您可以将扩展 lambda 传递给期望普通 lambda 的函数,只要参数列表彼此一致:
// ExtensionLambdas/Transform.kt
package extensionlambdas
import atomictest.eq
fun String.transform1(
n: Int, lambda: (String, Int) -> String
) = lambda(this, n)
fun String.transform2(
n: Int, lambda: String.(Int) -> String
) = lambda(this, n)
val duplicate: String.(Int) -> String = {
repeat(it)
}
val alternate: String.(Int) -> String = {
toCharArray()
.filterIndexed { i, _ -> i % it == 0 }
.joinToString("")
}
fun main() {
"hello".transform1(5, duplicate)
.transform2(3, alternate) eq "hleolhleo"
"hello".transform2(5, duplicate)
.transform1(3, alternate) eq "hleolhleo"
}
transform1()
期望普通的 lambda,而 transform2()
期望扩展 lambda。在 main()
中,扩展 lambda duplicate
和 alternate
都被传递给了 transform1()
和 transform2()
。在任一 lambda 传递给 transform1()
时,扩展 lambda 中的 this
接收者变成了第一个 String
参数。
使用 ::
,我们可以在期望扩展 lambda 的地方传递函数引用:
// ExtensionLambdas/FuncReferences.kt
package extensionlambdas
import atomictest.eq
fun Int.d1(f: (Int) -> Int) = f(this) * 10
fun Int.d2(f: Int.() -> Int) = f() * 10
fun f1(n: Int) = n + 3
fun Int.f2() = this + 3
fun main() {
74.d1(::f1) eq 770
74.d2(::f1) eq 770
74.d1(Int::f2) eq 770
74.d2(Int::f2) eq 770
}
对于扩展函数的引用与扩展 lambda 具有相同的类型:Int::f2
具有类型 Int.() -> Int
。
在调用中 74.d1(Int::f2)
,我们将一个扩展函数传递给了 d1()
,而该函数不声明扩展 lambda 参数。
多态适用于普通扩展函数(Base.g()
)和扩展 lambda(Base.h()
参数):
// ExtensionLambdas/ExtensionPolymorphism.kt
package extensionlambdas
import atomictest.eq
open class Base {
open fun f() = 1
}
class Derived : Base() {
override fun f() = 99
}
fun Base.g() = f()
fun Base.h(xl: Base.() -> Int) = xl()
fun main() {
val b: Base = Derived() // 向上转型
b.g() eq 99
b.h { f() } eq 99
}
您不会预料到它不起作用,但是通过创建一个示例来测试假设总是值得的。
您可以使用匿名函数语法(在 Local Functions 中描述)来替代扩展 lambda。在这里,我们使用了匿名扩展函数:
// ExtensionLambdas/AnonymousFunction.kt
package extensionlambdas
import atomictest.eq
fun exec(
arg1: Int, arg2: Int,
f: Int.(Int) -> Boolean
) = arg1.f(arg2)
fun main() {
exec(10, 2, fun Int.(d: Int): Boolean {
return this % d == 0
}) eq true
}
在 main()
中,对 exec()
的调用显示匿名扩展函数被接受为扩展 lambda。
Kotlin 标准库中包含许多使用扩展 lambda 的函数。例如,StringBuilder
是一个可修改的对象,当您调用 toString()
时,它会生成一个不可变的 String
。相比之下,更现代的 buildString()
接受一个扩展 lambda。它创建自己的 StringBuilder
对象,将扩展 lambda 应用于该对象,然后调用 toString()
产生结果:
// ExtensionLambdas/StringCreation.kt
package extensionlambdas
import atomictest.eq
private fun messy(): String {
val built = StringBuilder() // [1]
built.append("ABCs: ")
('a'..'x').forEach { built.append(it) }
return built.toString() // [2]
}
private fun clean() = buildString {
append("ABCs: ")
('a'..'x').forEach { append(it) }
}
private fun cleaner() =
('a'..'x').joinToString("", "ABCs: ")
fun main() {
messy() eq "ABCs: abcdefghijklmnopqrstuvwx"
messy() eq clean()
clean() eq cleaner()
}
在 messy()
中,我们多次重复了名称 built
。我们还必须创建一个 StringBuilder
([1]),并产生结果([2])。在 clean()
中使用 buildString()
,您不需要为 append()
调用创建和管理接收者,这使得一切更加简洁。
cleaner()
显示了,如果您愿意,有时可以找到一个更直接的解决方案,跳过构建器。
有与 buildString()
类似的标准库函数,它们使用扩展 lambda 生成已初始化的只读 List
和 Map
:
// ExtensionLambdas/ListsAndMaps.kt
@file:OptIn(ExperimentalStdlibApi::class)
package extensionlambdas
import atomictest.eq
val characters: List<String> = buildList {
add("Chars:")
('a'..'d').forEach { add("$it") }
}
val charmap: Map<Char, Int> = buildMap {
('A'..'F').forEachIndexed { n, ch ->
put(ch, n)
}
}
fun main() {
characters eq "[Chars:, a, b, c, d]"
// characters eq characters2
charmap eq "{A=0, B=1, C=2, D=3, E=4, F=5}"
}
在扩展 lambda 内部,List
和 Map
是可变的,但是 buildList
和 buildMap
的结果是只读的 List
和 Map
。
使用扩展 Lambda 编写构建器
假设您可以创建构造函数来生成所有必要的对象配置。有时候,可能的可能性太多,使得这变得混乱且不实际。构建器模式 具有以下几个优点:
- 它以多步过程创建对象。这在对象构建复杂的情
况下有时会很有帮助。 2. 它使用相同的基本构建代码来生成不同的对象变体。 3. 它将常见构建代码与特定代码分开,使编写和阅读各个对象变体的代码更加容易。
使用扩展 lambda 实现构建器提供了一个额外的好处,即创建了一个 领域特定语言(DSL)。DSL 的目标是对领域专家而非编程专家来说,具有舒适和合理的语法。这使得该用户只需了解一个小的语言子集,即可生成工作解决方案,同时从该语言的结构和安全性中受益。
例如,考虑一个捕获准备不同种类三明治的操作和成分的系统。我们可以使用类来建模 Recipe
的各个部分:
// ExtensionLambdas/Sandwich.kt
package sandwich
import atomictest.eq
open class Recipe : ArrayList<RecipeUnit>()
open class RecipeUnit {
override fun toString() =
"${this::class.simpleName}"
}
open class Operation : RecipeUnit()
class Toast : Operation()
class Grill : Operation()
class Cut : Operation()
open class Ingredient : RecipeUnit()
class Bread : Ingredient()
class PeanutButter : Ingredient()
class GrapeJelly : Ingredient()
class Ham : Ingredient()
class Swiss : Ingredient()
class Mustard : Ingredient()
open class Sandwich : Recipe() {
fun action(op: Operation): Sandwich {
add(op)
return this
}
fun grill() = action(Grill())
fun toast() = action(Toast())
fun cut() = action(Cut())
}
fun sandwich(
fillings: Sandwich.() -> Unit
): Sandwich {
val sandwich = Sandwich()
sandwich.add(Bread())
sandwich.toast()
sandwich.fillings()
sandwich.cut()
return sandwich
}
fun main() {
val pbj = sandwich {
add(PeanutButter())
add(GrapeJelly())
}
val hamAndSwiss = sandwich {
add(Ham())
add(Swiss())
add(Mustard())
grill()
}
pbj eq "[Bread, Toast, PeanutButter, " +
"GrapeJelly, Cut]"
hamAndSwiss eq "[Bread, Toast, Ham, " +
"Swiss, Mustard, Grill, Cut]"
}
sandwich()
捕获了生成任何 Sandwich
所需的基本成分和操作(在这里,我们假设所有三明治都是烤过的,但是在练习中,您将看到如何使其可选)。fillings
扩展 lambda 允许调用者以多种不同的方式配置 Sandwich
,但不需要为每个配置创建构造函数。
main()
中的语法显示了该系统可能如何用作 DSL——用户只需要理解通过调用 sandwich()
并在大括号中提供成分和操作的语法,即可创建 Sandwich
。
Exercises and solutions can be found at www.AtomicKotlin.com.