Kotlinの by は、処理を別のオブジェクトに委譲(delegation)するための構文です。
ソフトウェア設計では「委譲(Delegation)」というパターンがあり、これは
- 自分で処理を実装するのではなく
- 別のオブジェクトに処理を任せる
という設計方法です。
Kotlinでは、この委譲パターンを 非常に少ないコードで書けるように by が用意されています。
Kotlinにおける by の2つの用途
by は大きく次の 2種類の用途で使われます。
| 種類 | 内容 |
|---|---|
| クラス委譲 (Class Delegation) | インターフェース実装を別オブジェクトへ委譲 |
| プロパティ委譲 (Property Delegation) | プロパティの getter / setter を別オブジェクトへ委譲 |
よく使われる
lazyobservableMap
などは すべてプロパティ委譲の具体例です。
クラス委譲(Class Delegation)
概要
クラス委譲とは、インターフェースの実装を別のオブジェクトに任せる仕組みです。
基本構文
class クラス名(委譲オブジェクト) : Interface by 委譲オブジェクト
例
interface Printer {
fun print()
}
class PrinterImpl : Printer {
override fun print() {
println("print")
}
}
class Manager(printer: Printer) : Printer by printer
ここでは
ManagerはPrinterを実装している- しかし
print()の実装はprinterに委譲している
という構造になります。
コンパイラが行うこと
Kotlinコンパイラは、概念的には次のようなコードを生成します。
class Manager(private val printer: Printer) : Printer {
override fun print() {
printer.print()
}
}
つまり委譲先オブジェクトへメソッド呼び出しを転送するコードが自動生成されます。
一部のメソッドだけオーバーライド可能
委譲しつつ、特定のメソッドだけ自分で実装することもできます。
class Manager(printer: Printer) : Printer by printer {
override fun print() {
println("manager print")
}
}
この場合
print()→ Manager の実装- その他のメソッド → 委譲
になります。
注意点
クラス委譲は 継承とは挙動が異なる部分があります。
委譲先オブジェクト内部で呼ばれるメソッドは、派生クラスの override を使用しない場合があります。
つまり
- 委譲元で override したメソッド
- 委譲先内部の呼び出し
は一致しない場合があります。
これは 委譲は継承ではないという設計によるものです。
プロパティ委譲
概要
プロパティ委譲はプロパティの getter / setter の処理を別オブジェクトに任せる仕組みです。
基本構文
var propertyName by delegate
または
val propertyName by delegate
デリゲートオブジェクト
デリゲートは次の関数を提供します。
operator fun getValue(...)
operator fun setValue(...)
例
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Hello"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("value set $value")
}
}
使用例
var name: String by Delegate()
println(name)
name = "Taro"
動作の仕組み
読み込み時
name
↓
delegate.getValue(...)
書き込み時
name = value
↓
delegate.setValue(...)
このようにgetter / setter が delegate に委譲されます。
lazy
lazy は プロパティ委譲の代表例です。
例
val data by lazy {
println("init")
"Hello"
}
動作
println(data)
println(data)
出力
init
Hello
Hello
特徴
- 初回アクセス時だけ初期化
- 以降は同じ値を返す
なぜ便利なのか
通常
val data = loadData()
は オブジェクト生成時に実行されます。
しかし lazy を使うと必要になるまで処理を遅らせることができます。
observable
プロパティ変更を監視できます。
例
import kotlin.properties.Delegates
var name: String by Delegates.observable("initial") { property, old, new ->
println("$old -> $new")
}
実行
name = "Taro"
name = "Jiro"
出力
initial -> Taro
Taro -> Jiro
Map委譲
Mapの値をそのままプロパティとして扱えます。
例
class User(private val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
使用
val user = User(
mapOf(
"name" to "Taro",
"age" to 20
)
)
println(user.name)
この場合
map["name"]
が自動的に取得されます。
別プロパティへの委譲
Kotlinでは 別のプロパティに委譲することも可能です。
class Example {
var newName: Int = 0
var oldName by this::newName
}
ここでは
oldName
は
newName
と同じプロパティになります。
Kotlinで by が重要な理由
Kotlinはボイラープレートコード削減を重視した言語です。
Javaで委譲を書くと次のようになります。
class Manager implements Printer {
private Printer printer;
Manager(Printer printer) {
this.printer = printer;
}
public void print() {
printer.print();
}
}
Kotlinでは
class Manager(printer: Printer) : Printer by printer
これだけで書けます。
Kotlinの設計思想
by は次の設計思想を強く反映しています。
Composition over inheritance
(継承より委譲)
つまり
- 継承を増やすより
- オブジェクトを組み合わせる
という設計です。
まとめ
| 機能 | 内容 |
|---|---|
| クラス委譲 | インターフェース実装を別オブジェクトへ委譲 |
| プロパティ委譲 | getter / setter を別オブジェクトへ委譲 |
| lazy | 遅延初期化 |
| observable | プロパティ変更監視 |
| Map委譲 | Mapの値をプロパティ化 |
以上、Kotlinのbyについてでした。
最後までお読みいただき、ありがとうございました。










