3.3.3 带接收者的 Lambda 表达式

原文链接: https://kotlinlang.org/docs/kotlin-tour-intermediate-lambdas-receiver.html

3.3.3 带接收者的 Lambda 表达式

本章将学习如何把接收者用于另一种函数——lambda 表达式,以及它们如何帮助你创建领域特定语言。

带接收者的 Lambda 表达式

在入门导览中,你已经学会了如何使用 lambda 表达式。lambda 表达式也可以有接收者。在这种情况下,lambda 表达式可以访问接收者的任何成员函数或属性,而不必每次都显式指定接收者。不用写这些额外的引用,代码就更易读、更易维护。

提示: 带接收者的 lambda 表达式也称为带接收者的函数字面量。

在定义函数类型时,带接收者的 lambda 表达式语法有所不同。首先写出你想扩展的接收者,接着写一个 .,然后完成函数类型定义的其余部分。例如:

1
MutableList<Int>.() -> Unit

这个函数类型具有:

  • MutableList<Int> 作为接收者。
  • 圆括号 () 中没有函数参数。
  • 没有返回值:Unit。

请看下面这个在画布上绘制图形的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Canvas {
    fun drawCircle() = println("🟠 Drawing a circle")
    fun drawSquare() = println("🟥 Drawing a square")
}

// Lambda expression with receiver definition
fun render(block: Canvas.() -> Unit): Canvas {
    val canvas = Canvas()
    // Use the lambda expression with receiver
    canvas.block()
    return canvas
}

fun main() {
    render {
        drawCircle()
        // 🟠 Drawing a circle
        drawSquare()
        // 🟥 Drawing a square
    }
}

在这个示例中:

  • Canvas 类有两个函数,分别模拟绘制圆形或正方形。
  • render() 函数接收一个 block 参数,并返回 Canvas 类的一个实例。
  • block 参数是一个带接收者的 lambda 表达式,其中 Canvas 类就是接收者。
  • render() 函数创建一个 Canvas 类的实例,并在 canvas 实例上以它为接收者调用 block() lambda 表达式。
  • main() 函数用一个 lambda 表达式调用 render() 函数,该 lambda 表达式被传给 block 参数。
  • 在传给 render() 函数的 lambda 内部,程序在 Canvas 类的实例上调用 drawCircle() 和 drawSquare() 函数。

由于 drawCircle() 和 drawSquare() 函数是在带接收者的 lambda 表达式中调用的,它们可以像在 Canvas 类内部那样被直接调用。

当你想创建领域特定语言(DSL)时,带接收者的 lambda 表达式会很有帮助。由于无需显式引用接收者就能访问它的成员函数和属性,代码会变得更精炼。

为了演示这一点,来看一个配置菜单项目的示例。我们先定义一个 MenuItem 类,以及一个 Menu 类,后者包含一个用于向菜单添加项目的函数 item(),以及一个包含所有菜单项目的列表 items:

1
2
3
4
5
6
7
8
9
class MenuItem(val name: String)

class Menu(val name: String) {
    val items = mutableListOf<MenuItem>()

    fun item(name: String) {
        items.add(MenuItem(name))
    }
}

接下来,我们用一个作为函数参数(init)传给 menu() 函数的带接收者的 lambda 表达式,作为构建菜单的起点:

1
2
3
4
5
6
7
fun menu(name: String, init: Menu.() -> Unit): Menu {
    // Creates an instance of the Menu class
    val menu = Menu(name)
    // Calls the lambda expression with receiver init() on the class instance
    menu.init()
    return menu
}

现在你就可以用这个 DSL 配置菜单,并创建一个把菜单结构打印到控制台的 printMenu() 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MenuItem(val name: String)

class Menu(val name: String) {
    val items = mutableListOf<MenuItem>()

    fun item(name: String) {
        items.add(MenuItem(name))
    }
}

fun menu(name: String, init: Menu.() -> Unit): Menu {
    val menu = Menu(name)
    menu.init()
    return menu
}

fun printMenu(menu: Menu) {
    println("Menu: ${menu.name}")
    menu.items.forEach { println("  Item: ${it.name}") }
}

// Use the DSL
fun main() {
    // Create the menu
    val mainMenu = menu("Main Menu") {
        // Add items to the menu
        item("Home")
        item("Settings")
        item("Exit")
    }

    // Print the menu
    printMenu(mainMenu)
    // Menu: Main Menu
    //   Item: Home
    //   Item: Settings
    //   Item: Exit
}

如你所见,使用带接收者的 lambda 表达式极大地简化了创建菜单所需的代码。lambda 表达式不仅适用于初始化和创建,也适用于配置。它们常被用于构建 API、UI 框架和配置构建器的 DSL,从而产出更简洁的代码,让你更容易专注于底层的代码结构和逻辑。

Kotlin 生态中有许多这种设计模式的例子,例如标准库中的 buildList() 和 buildString() 函数。

提示: 带接收者的 lambda 表达式可以与 Kotlin 的类型安全构建器结合使用,从而构建出在编译期而非运行期就能发现类型问题的 DSL。要了解更多内容,请参阅类型安全构建器。

练习

习题 1

你有一个 fetchData() 函数,它接受一个带接收者的 lambda 表达式。请修改这个 lambda 表达式,使用 append() 函数,使代码输出为:Data received - Processed。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun fetchData(callback: StringBuilder.() -> Unit) {
    val builder = StringBuilder("Data received")
    builder.callback()
}

fun main() {
    fetchData {
        // Write your code here
        // Data received - Processed
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun fetchData(callback: StringBuilder.() -> Unit) {
    val builder = StringBuilder("Data received")
    builder.callback()
}

fun main() {
    fetchData {
        append(" - Processed")
        println(this.toString())
        // Data received - Processed
    }
}

示例解答

习题 2

你有一个 Button 类,以及 ButtonEvent 和 Position 两个数据类。请编写一些代码来触发 Button 类的 onEvent() 成员函数,从而触发一次双击事件。你的代码应打印 "Double click!"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Button {
    fun onEvent(action: ButtonEvent.() -> Unit) {
        // Simulate a double-click event (not a right-click)
        val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
        event.action() // Trigger the event callback
    }
}

data class ButtonEvent(
    val isRightClick: Boolean,
    val amount: Int,
    val position: Position
)

data class Position(
    val x: Int,
    val y: Int
)

fun main() {
    val button = Button()

    button.onEvent {
        // Write your code here
        // Double click!
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Button {
    fun onEvent(action: ButtonEvent.() -> Unit) {
        // Simulate a double-click event (not a right-click)
        val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
        event.action() // Trigger the event callback
    }
}

data class ButtonEvent(
    val isRightClick: Boolean,
    val amount: Int,
    val position: Position
)

data class Position(
    val x: Int,
    val y: Int
)

fun main() {
    val button = Button()

    button.onEvent {
        if (!isRightClick && amount == 2) {
            println("Double click!")
            // Double click!
        }
    }
}

示例解答

习题 3

编写一个函数,创建一份整数列表的副本,其中每个元素都加 1。请使用给出的函数骨架,它通过 incremented 函数扩展 List<Int>。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun List<Int>.incremented(): List<Int> {
    val originalList = this
    return buildList {
        // Write your code here
    }
}

fun main() {
    val originalList = listOf(1, 2, 3)
    val newList = originalList.incremented()
    println(newList)
    // [2, 3, 4]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun List<Int>.incremented(): List<Int> {
    val originalList = this
    return buildList {
        for (n in originalList) add(n + 1)
    }
}

fun main() {
    val originalList = listOf(1, 2, 3)
    val newList = originalList.incremented()
    println(newList)
    // [2, 3, 4]
}

示例解答

另请参阅

上一步

下一步