# 带接受者的 Lambda 表达式 在这一章中, 你将学习在另一种函数类型 Lambda 表达式中如何使用接受者, 以及它们如何帮助你创建一个特定领域专用语言(Domain-Specific Language, DSL). ## 带接受者的 Lambda 表达式 在初学者教程中, 你已经学习了如何使用 [Lambda 表达式](kotlin-tour-functions.html#lambda-expressions). Lambda 表达式也可以带有接受者. 这种情况下, Lambda 表达式能够访问接受者的任何成员函数或属性, 而不必每次都明确的指明接受者. 没有了这些额外的引用, 你的代码会变得更加易于阅读和维护. > **Tip:** > 带接受者的 Lambda 表达式也叫做带接受者的函数字面值. 带接受者的 Lambda 表达式的语法与定义函数类型时不同. 首先, 请写下你想要扩展的接受者. 之后, 是一个 `.` 号, 之后写下你的函数类型定义的其它部分. 例如: ```KOTLIN MutableList.() -> Unit ``` 这个函数类型: * 接受者是 `MutableList`. * 括号 `()` 之内没有函数参数. * 没有返回值: `Unit`. 我们来看看下面的实例, 它在画布上绘制图形: ```KOTLIN class Canvas { fun drawCircle() = println("🟠 Drawing a circle") fun drawSquare() = println("🟥 Drawing a square") } // 带接受者的 Lambda 表达式定义 fun render(block: Canvas.() -> Unit): Canvas { val canvas = Canvas() // 使用带接受者的 Lambda 表达式 canvas.block() return canvas } fun main() { render { drawCircle() // 输出结果为: 🟠 Drawing a circle drawSquare() // 输出结果为: 🟥 Drawing a square } } ``` 在这个示例中: * `Canvas` 类有 2 个函数, 模拟绘制圆形和正方形. * `render()` 函数接受一个 `block` 参数, 返回一个 `Canvas` 类的实例. * `block` 参数是一个带接受者的 Lambda 表达式, 其中 `Canvas` 类是接受者. * `render()` 函数创建一个 `Canvas` 类的实例, 并使用它作为接受者, 在 `canvas` 实例上调用 `block()` Lambda 表达式. * `main()` 函数调用 `render()` 函数, 使用 Lambda 表达式, 传递给 `block` 参数. * 在传递给 `render()` 函数的 Lambda 表达式内, 程序会在 `Canvas` 类的实例上调用 `drawCircle()` 和 `drawSquare()` 函数. 由于 `drawCircle()` 和 `drawSquare()` 函数是在带接受者的 Lambda 表达式之内调用, 因此可以象在 `Canvas` 类之内一样调用它们. 如果你想要创建一个特定领域专用语言(Domain-Specific Language, DSL), 带接受者的 Lambda 表达式会非常有用. 因为你可以访问接受者的成员函数和属性, 而不必明确引用接受者, 你的代码会变得更加精简. 为了演示这一点, 我们来考虑一个配置菜单中项目的示例. 我们从一个 `MenuItem` 类和一个 `Menu` 类开始, `Menu` 类包含一个向菜单中添加项目的函数, 名为 `item()`, 以及一个包含所有项目的列表, 名为 `items`: ```KOTLIN class MenuItem(val name: String) class Menu(val name: String) { val items = mutableListOf() fun item(name: String) { items.add(MenuItem(name)) } } ``` 我们使用一个带接受者的 Lambda 表达式, 将它作为函数参数 (`init`) 传递给 `menu()` 函数, 这个函数构建一个菜单, 作为开始点: ```KOTLIN fun menu(name: String, init: Menu.() -> Unit): Menu { // 创建 Menu 类的一个实例 val menu = Menu(name) // 在类实例上调用带接受者的 Lambda 表达式 init() menu.init() return menu } ``` 现在你可以使用这个 DSL 来配置一个菜单, 并创建一个 `printMenu()` 函数, 将菜单结构打印输出到控制台: ```KOTLIN class MenuItem(val name: String) class Menu(val name: String) { val items = mutableListOf() fun item(name: String) { items.add(MenuItem(name)) } } fun menu(name: String, init: Menu.() -> Unit): Menu { val menu = Menu(name) menu.init() return menu } //sampleStart fun printMenu(menu: Menu) { println("Menu: ${menu.name}") menu.items.forEach { println(" Item: ${it.name}") } } // 使用 DSL fun main() { // 创建菜单 val mainMenu = menu("Main Menu") { // 向菜单添加项目 item("Home") item("Settings") item("Exit") } // 打印菜单 printMenu(mainMenu) // 输出结果为: // Menu: Main Menu // Item: Home // Item: Settings // Item: Exit } //sampleEnd ``` 如你所见, 使用带接受者的 Lambda 表达式 大大的简化了创建你的菜单所需要的代码. Lambda 表达式不仅仅可以用于设置和创建, 也可以用于配置. 它们普遍用于构建 API, UI 框架, 以及配置构建器的 DSL, 以生成精简的代码, 让你能够更容易的专注于底层代码结构和逻辑. Kotlin 的生态环境中存在这种设计模式的很多例子, 例如标准库中的 [buildList()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/build-list.html) 和 [buildString()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/build-string.html) 函数. > **Tip:** > 在 Kotlin 中, 带接受者的 Lambda 表达式可以与 类型安全的构建器 结合, 创建能够在编译期而不是在运行期检测出类型问题的 DSL. 详情请参见 [类型安全的构建器](type-safe-builders.html). ## 实际练习 ### 习题 1 你有一个 `fetchData()` 函数, 参数是一个带接受者的 Lambda 表达式. 请更新 Lambda 表达式, 使用 `append()` 函数, 让你的代码的输出成为: `Data received - Processed`. ```KOTLIN fun fetchData(callback: StringBuilder.() -> Unit) { val builder = StringBuilder("Data received") builder.callback() } fun main() { fetchData { // 请在这里编写你的代码 // 输出结果为: Data received - Processed } } ``` ```KOTLIN 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 事件. 你的代码应该打印输出 `"Double click!"`. ```KOTLIN class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // 模拟 double-click 事件 (不是 right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // 触发事件回调 } } 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 { // 请在这里编写你的代码 // 输出结果为: Double click! } } ``` ```KOTLIN class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // 模拟 double-click 事件 (不是 right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // 触发事件回调 } } 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 编写一个函数, 创建一个整数 List 的副本, 其中每个元素增大 1. 请使用已经提供的函数框架, 它使用一个 `incremented` 函数扩展了 `List`. ```KOTLIN fun List.incremented(): List { val originalList = this return buildList { // 请在这里编写你的代码 } } fun main() { val originalList = listOf(1, 2, 3) val newList = originalList.incremented() println(newList) // 输出结果为: [2, 3, 4] } ``` ```KOTLIN fun List.incremented(): List { 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] } ``` ## See also * [上一步](kotlin-tour-intermediate-scope-functions.html) * [下一步](kotlin-tour-intermediate-classes-interfaces.html)