中級:帶接收者的 Lambda 表達式
在本章中,你將學習如何在另一種函數類型——lambda 表達式——中使用接收者,以及它們如何幫助你建立領域特定語言。
帶接收者的 Lambda 表達式
在初學者課程中,你學習了如何使用 lambda 表達式。Lambda 表達式也可以有接收者。在這種情況下,lambda 表達式可以存取接收者的任何成員函數或屬性,而無需每次都顯式指定接收者。少了這些額外的引用,你的程式碼將更容易閱讀和維護。
帶接收者的 Lambda 表達式也稱為帶接收者的函數字面值。
當你定義函數類型時,帶接收者的 lambda 表達式的語法是不同的。首先,寫下你要擴展的接收者。接著,放一個 .
,然後完成你函數類型的其餘定義。例如:
MutableList<Int>.() -> Unit
這個函數類型具有:
MutableList<Int>
作為接收者。- 在圓括號
()
內沒有函數參數。 - 沒有返回值:
Unit
。
考慮這個在畫布上繪製形狀的範例:
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()
函數呼叫render()
函數並帶有一個 lambda 表達式,該 lambda 表達式被傳遞給block
參數。在傳遞給
render()
函數的 lambda 內部,程式碼在Canvas
類別的實例上呼叫了drawCircle()
和drawSquare()
函數。因為
drawCircle()
和drawSquare()
函數是在帶接收者的 lambda 表達式中被呼叫的,它們可以直接被呼叫,就像它們在Canvas
類別內部一樣。
帶接收者的 lambda 表達式在你想建立領域特定語言 (DSL) 時很有幫助。由於你可以存取接收者的成員函數和屬性,而無需顯式引用接收者,你的程式碼變得更精簡。
為了證明這一點,考慮一個設定選單中項目的範例。讓我們從一個 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))
}
}
讓我們使用一個作為函數參數 (init
) 傳遞給 menu()
函數的帶接收者的 lambda 表達式,該函數作為起點建構一個選單:
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()
函數來將選單結構印到控制台:
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
。
fun fetchData(callback: StringBuilder.() -> Unit) {
val builder = StringBuilder("Data received")
builder.callback()
}
fun main() {
fetchData {
// Write your code here
// 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) {
// 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!
}
}
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。使用提供的函數骨架,它擴展 List<Int>
並包含 incremented
函數。
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]
}
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]
}