进阶:带接收者的 lambda 表达式
在本章中,你将学习如何在另一种函数类型——lambda 表达式——中使用接收者对象,以及它们如何帮助你创建领域特定语言。
带接收者的 lambda 表达式
在入门教程中,你学习了如何使用 lambda 表达式。Lambda 表达式也可以拥有接收者。在这种情况下,lambda 表达式可以访问接收者对象的任何成员函数或属性,而无需每次都显式指定接收者对象。没有这些额外的引用,你的代码将更易于阅读和维护。
带接收者的 lambda 表达式也称为函数字面量带接收者。
定义函数类型时,带接收者的 lambda 表达式的语法有所不同。首先,编写你想要扩展的接收者类型。接下来,放置一个 .
,然后完成你的函数类型定义的其余部分。例如:
MutableList<Int>.() -> Unit
此函数类型有:
MutableList<Int>
作为接收者类型。- 圆括号
()
内没有函数形参。 - 没有返回值:
Unit
。
考虑以下扩展 StringBuilder
类的示例:
fun main() {
// 带接收者的 lambda 表达式定义
fun StringBuilder.appendText() { append("Hello!") }
// 使用带接收者的 lambda 表达式
val stringBuilder = StringBuilder()
stringBuilder.appendText()
println(stringBuilder.toString())
// Hello!
}
在此示例中:
StringBuilder
类是接收者类型。- lambda 表达式的函数类型没有函数形参
()
也没有返回值Unit
。 - lambda 表达式调用
StringBuilder
类中的append()
成员函数,并使用字符串"Hello!"
作为函数形参。 - 创建了
StringBuilder
类的一个实例。 - 在
stringBuilder
实例上调用了赋值给appendText
的 lambda 表达式。 stringBuilder
实例通过toString()
函数转换为字符串,并使用println()
函数打印。
当你想要创建领域特定语言(DSL)时,带接收者的 lambda 表达式会很有帮助。由于你可以访问接收者对象的成员函数和属性,而无需显式引用接收者,因此你的代码会变得更简洁。
为了演示这一点,考虑一个配置菜单项的示例。让我们从一个 MenuItem
类和一个 Menu
类开始,Menu
类包含一个用于向菜单添加项的 item()
函数,以及所有菜单项的 items
列表:
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf<MenuItem>()
fun item(name: String) {
items.add(MenuItem(name))
}
}
让我们使用一个带接收者的 lambda 表达式,将其作为函数形参 (init
) 传递给 menu()
函数,该函数将菜单作为起点进行构建。你会注意到代码遵循了与之前 StringBuilder
示例类似的方法:
fun menu(name: String, init: Menu.() -> Unit): Menu {
// 创建 Menu 类的实例
val menu = Menu(name)
// 在类实例上调用带接收者的 lambda 表达式 init()
menu.init()
return menu
}
现在你可以使用 DSL 来配置菜单,并创建一个 printMenu()
函数来将菜单结构打印到控制台:
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}") }
}
// 使用 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
}
如你所见,使用带接收者的 lambda 表达式极大地简化了创建菜单所需的代码。Lambda 表达式不仅对设置和创建有用,也对配置有用。它们常用于构建 API、UI 框架和配置构建器的 DSL,以生成精简的代码,让你更容易专注于底层代码结构和逻辑。
Kotlin 的生态系统中有许多此设计模式的示例,例如标准库中的 buildList()
和 buildString()
函数。
带接收者的 lambda 表达式可以与 Kotlin 中的类型安全的构建器结合使用,以创建可以在编译期而非运行时检测类型问题的 DSL。关于更多信息,请参见 类型安全的构建器。
练习
练习 1
你有一个 fetchData()
函数,它接受一个带接收者的 lambda 表达式。更新 lambda 表达式以使用 append()
函数,使你的代码输出为:Data received - Processed
。
fun fetchData(callback: StringBuilder.() -> Unit) {
val builder = StringBuilder("Data received")
builder.callback()
}
fun main() {
fetchData {
// 在此处编写你的代码
// Data received - Processed
}
}
示例解决方案
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!"
。
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// 模拟双击事件(非右击)
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!
}
}
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// 模拟双击事件(非右击)
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。使用提供的扩展 List<Int>
的 incremented
函数骨架。
fun List<Int>.incremented(): List<Int> {
val originalList = this
return buildList {
// 在此处编写你的代码
}
}
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}
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]
}