中級: レシーバー付きラムダ式
拡張関数
スコープ関数
レシーバー付きラムダ式
クラスとインターフェース
オブジェクト
オープンクラスと特殊なクラス
プロパティ
null安全
ライブラリとAPI
この章では、別の種類の関数であるラムダ式でレシーバーを使用する方法と、それがドメイン固有言語を作成するのにどのように役立つかを学習します。
レシーバー付きラムダ式
入門編では、ラムダ式の使用方法を学習しました。ラムダ式もレシーバーを持つことができます。 この場合、ラムダ式は、レシーバーを毎回明示的に指定することなく、レシーバーのメンバー関数やプロパティにアクセスできます。これらの余分な参照がないため、コードは読みやすく、保守しやすくなります。
レシーバー付きラムダ式は、レシーバー付き関数リテラルとも呼ばれます。
レシーバー付きラムダ式の構文は、関数型を定義するときに異なります。まず、拡張したいレシーバーを記述します。次に、.
を記述し、残りの関数型の定義を完了します。例:
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))
}
}
開始点として、メニューを構築する menu()
関数に、関数パラメータ (init
) として渡されるレシーバー付きラムダ式を使用してみましょう。
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()
関数があります。コードの出力が Data received - Processed
になるように、ラムダ式を append()
関数を使用するように更新してください。
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]
}