委譲プロパティ (Delegated Properties)
必要なときに手動で実装することはできますが、一度実装してライブラリに入っていると非常にうれしいといった、ある種のよくある一般的なのプロパティはあります。例としては、
- 遅延プロパティ (lazy properties) :値は最初のアクセス時に初めて計算されます
- オブザーバブルプロパティ (observable properties) :リスナがこのプロパティの変更に関する通知を受け取ります
- フィールドとは分かれていない、map内でのストロングプロパティ (strong properties)
これら(およびその他)のケースをカバーするために、Kotlinは、 委譲プロパティ (delegated properties) をサポートしています。
class Example {
var p: String by Delegate()
}
構文は次のとおりです val/var <property name>: <Type> by <expression>
get()
(と set()
)はその getValue()
および setValue()
メソッドに委譲されるプロパティに対応するため、 by
の後に続く式は、 委譲 (delegate) です。プロパティの委譲には、任意のインターフェイスを実装する必要はありませんが、 getValue()
関数(そして setValue()
— var用に)を提供する必要があります。例えば:
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'")
}
}
Delegate インスタンスのデリゲートである p
を読み込むとき、 Delegate
のgetValue()
関数が呼び出されています。そのため、その最初のパラメータは、 p
を読み取る先のオブジェクトであり、2番目のパラメータは、 p
自体の説明を保持しています(例えば、あなたがその名前を得ることができます)。例えば:
val e = Example()
println(e.p)
これは次の通り出力します
Example@33a17727, thank you for delegating ‘p’ to me!
p
に代入するのと同様に、setValue()
関数が呼び出されます。最初の2つのパラメータは同じであり、3つ目は、割り当てられた値を保持します。
e.p = "NEW"
これは次の通り出力します
NEW has been assigned to ‘p’ in Example@33a17727.
プロパティデリゲートの要件
ここでは、オブジェクトを委譲するための要件をまとめます。
読み取り専用プロパティ(すなわち val)のために、デリゲートは、次のパラメータを取る getValue
という名前の関数を提供する必要があります。
- レシーバ — プロパティ所有者 のものと同じかスーパータイプでなければなりません(拡張プロパティー — 拡張されるタイプの場合)。
- メタデータ — 型
KProperty <*>
またはそのスーパータイプでなければなりません。
この関数は、プロパティ(またはそのサブタイプ)と同じ型を返さなければなりません。
変更可能な プロパティ ( var ) の場合、デリゲートは、さらに次のパラメータを取り setValue
という名前の関数を 追加で 提供する必要があります。
- レシーバ —
getValue()
と同じ - メタデータ —
getValue()
と同じ - 新しい値 — プロパティまたはそのスーパータイプと同じタイプでなければなりません。
getValue()
および/または setValue()
関数は、いずれかの委譲クラスや拡張機能のメンバ関数として提供することができます。これらの機能を提供していないオブジェクトにプロパティを委譲する必要がある場合、後者が便利です。関数の両方を operator
キーワードでマークする必要があります。
Kotlin標準ライブラリでは、いくつかの有用なデリゲートのファクトリメソッドを提供します。
遅延 (lazy)
lazy()
はラムダをとり、遅延プロパティを実装するためのデリゲートとして機能する Lazy<T>
のインスタンスを返す関数です。get()
の最初の呼び出しは lazy()
に渡されたラムダを実行し、結果を保持します。 それ以降、get()
を呼び出すと、単に記憶された結果が返されます。
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}
デフォルトでは、遅延特性の評価が 同期されます 。値は1つのスレッドで計算され、すべてのスレッドで同じ値が表示されます。もし初期化デリゲートの同期が必要ではない場合は、 複数のスレッドが同時に初期化を実行できるように LazyThreadSafetyMode.PUBLICATION
を lazy()
関数のパラメータとして渡します。初期化が常に単一のスレッドで起こると確信しているなら、任意のスレッドの安全性の保証および関連するオーバーヘッドが発生しない LazyThreadSafetyMode.NONE
モードを使用することができます。
オブザーバブル (Observable)
Delegates.observable()
は、2つの引数を取ります。初期値と修正のためのハンドラです。ハンドラは(割り当てが行われた 後 に)プロパティに割り当てるたびに呼び出されます。それには3つのパラメータがあり、割り当てられているプロパティ、古い値、そして新しい値です:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
この例の出力:
<no name> -> first
first -> second
もし代入を傍受し、それに対し 「拒否権」を発動できるようにしたい場合は、observable() の代わりに vetoable()
を使います。 vetoable
に渡されたハンドラは、新しいプロパティ値の割り当てが行われる 前 に呼び出されます。
Map 中のストアリングプロパティ (Storing Properties in a Map)
一般的な使用例のひとつとして、map 内のプロパティの値を記憶することが挙げられます。これはJSONをパースしたり、他の「動的」なことをやるようなアプリケーションで頻繁に起こっています。この事例では、委譲プロパティのデリゲートとして map のインスタンス自体を使用することができます。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
この例では、コンストラクタは、 map を取ります。
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
委譲プロパティは、このマップから(文字列 — この場合プロパティの名前 — のキーを使って)値を取ります:
println(user.name) // 出力:"John Doe"
println(user.age) // 出力:25
読み取り専用 Map
の代わりに MutableMap
を使用すると、これは var のプロパティに対しても動作します:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}