Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

扩展 Lambda

扩展 Lambda 类似于扩展函数。它定义了一个 lambda,而不是一个函数。

在这里,vavb 产生相同的结果:

// 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 ->

vbString 参数移到括号外,并使用扩展函数语法: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 duplicatealternate 都被传递给了 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 生成已初始化的只读 ListMap

// 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 内部,ListMap 是可变的,但是 buildListbuildMap 的结果是只读的 ListMap

使用扩展 Lambda 编写构建器

假设您可以创建构造函数来生成所有必要的对象配置。有时候,可能的可能性太多,使得这变得混乱且不实际。构建器模式 具有以下几个优点:

  1. 它以多步过程创建对象。这在对象构建复杂的情

况下有时会很有帮助。 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.