中級: レシーバ付きラムダ式
拡張関数
スコープ関数
レシーバ付きラムダ式
クラスとインターフェース
オブジェクト
Openクラスと特殊なクラス
プロパティ
Null安全
ライブラリとAPI
この章では、別の種類の関数であるレシーバ付きラムダ式の使い方と、それがドメイン固有言語(DSL)の作成にどのように役立つかを学びます。
レシーバ付きラムダ式
入門ツアーでは、ラムダ式の使い方を学びました。ラムダ式にはレシーバを持たせることもできます。 この場合、ラムダ式の中では、毎回明示的にレシーバを指定することなく、レシーバの任意のメンバ関数やプロパティにアクセスできます。これらの追加の参照が不要になることで、コードの可読性と保守性が向上します。
レシーバ付きラムダ式は、レシーバ付き関数リテラルとも呼ばれます。
レシーバ付きラムダ式の構文は、関数型を定義する際に異なります。まず、拡張したいレシーバを記述します。次に . を置き、その後に残りの関数型の定義を続けます。例えば以下のようになります。
MutableList<Int>.() -> Unitこの関数型の構成は以下の通りです。
MutableList<Int>がレシーバです。- 括弧
()内に関数パラメータはありません。 - 戻り値はありません(
Unit)。
キャンバスに図形を描画する以下の例を考えてみましょう。
class Canvas {
fun drawCircle() = println("🟠 Drawing a circle")
fun drawSquare() = println("🟥 Drawing a square")
}
// レシーバ付きラムダ式の定義
fun render(block: Canvas.() -> Unit): Canvas {
val canvas = Canvas()
// レシーバ付きラムダ式を使用
canvas.block()
return canvas
}
fun main() {
render {
drawCircle()
// 🟠 Drawing a circle
drawSquare()
// 🟥 Drawing a square
}
}この例では:
Canvasクラスには、円や正方形の描画をシミュレートする2つの関数があります。render()関数はblockパラメータを受け取り、Canvasクラスのインスタンスを返します。blockパラメータはレシーバ付きラムダ式であり、Canvasクラスがレシーバとなっています。render()関数はCanvasクラスのインスタンスを作成し、そのcanvasインスタンスをレシーバとしてblock()ラムダ式を呼び出します。main()関数は、blockパラメータに渡されるラムダ式を伴ってrender()関数を呼び出します。render()関数に渡されたラムダ式の内部では、プログラムはCanvasクラスのインスタンスに対してdrawCircle()とdrawSquare()関数を呼び出します。drawCircle()とdrawSquare()関数はレシーバ付きラムダ式の中で呼び出されているため、あたかもCanvasクラスの内部にいるかのように直接呼び出すことができます。
レシーバ付きラムダ式は、ドメイン固有言語(DSL)を作成したい場合に役立ちます。レシーバを明示的に参照することなくレシーバのメンバ関数やプロパティにアクセスできるため、コードがより簡潔になります。
これを実証するために、メニューの項目を設定する例を考えてみましょう。まず、MenuItem クラスと、メニューに項目を追加する item() 関数、および全メニュー項目のリスト items を持つ Menu クラスから始めます。
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf<MenuItem>()
fun item(name: String) {
items.add(MenuItem(name))
}
}メニューを構築する出発点として、関数パラメータ (init) として渡されたレシーバ付きラムダ式を使用する menu() 関数を使用します。
fun menu(name: String, init: Menu.() -> Unit): Menu {
// Menuクラスのインスタンスを作成
val menu = Menu(name)
// クラスインスタンスに対してレシーバ付きラムダ式 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
}見ての通り、レシーバ付きラムダ式を使用することで、メニューを作成するために必要なコードが大幅に簡素化されます。ラムダ式は、セットアップや作成だけでなく、設定にも役立ちます。これらは、API、UI フレームワーク、設定ビルダーのための DSL 構築に一般的に使用され、合理化されたコードを作成することで、基盤となるコード構造やロジックにより集中できるようにします。
Kotlin のエコシステムには、標準ライブラリの buildList() や buildString() 関数など、このデザインパターンの例が多くあります。
レシーバ付きラムダ式を Kotlin の型安全なビルダーと組み合わせることで、実行時ではなくコンパイル時に型に関する問題を検出できる DSL を作成できます。詳細については、型安全なビルダーを参照してください。
練習問題
練習問題 1
レシーバ付きラムダ式を受け取る fetchData() 関数があります。ラムダ式を更新して 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
整数のリストのコピーを作成し、各要素を 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]
}