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]
}
|
示例解答
另请参阅
上一步
下一步