委譲プロパティ (Delegated Properties)
ある種のプロパティは、 必要なときに毎回手作りで実装することもできなくは無いけれど、 一度実装してライブラリに入れて、それをあとで再利用出来る方が嬉しい事があります。 例としては:
- 遅延プロパティ (lazy properties) :値は最初のアクセス時に初めて計算されます
- observableプロパティ:リスナがこのプロパティの変更に関する通知を受け取ります
- 各プロパティをそれぞれ別のフィールドにはせずに、マップに保存するようなプロパティ
これら(およびその他)のケースをカバーするために、Kotlinは、 委譲プロパティ (delegated properties) をサポートしています。
class Example {
var p: String by Delegate()
}
構文は次のとおりです val/var <プロパティ名>: <型> by <式>。
byの後の式は_委譲(delegate)_です。
というのは、そのプロパティに対応したget() (と set() )は、
その式の getValue() および setValue() メソッドに委譲されるからです。
プロパティの委譲には、特別なインターフェイスを実装する必要はありませんが、
getValue() 関数(そしてvarの場合は setValue()関数)を提供する必要があります。例えば:
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, '${property.name}'を私に委譲してくれてありがとう!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value は $thisRef の '${property.name}' に代入された。")
}
}
p を読み込むと、Delegateのインスタンスに委譲されて、DelegateのgetValue()が呼ばれます。
その最初のパラメータは、 p を読み取る対象となるオブジェクトであり、2番目のパラメータは、 p 自体の情報を保持しています(例えば、そこから名前を得ることができます)。例えば:
val e = Example()
println(e.p)
これは次の通り出力します
Example@33a17727, 'p'を私に委譲してくれてありがとう!
同様に、p に代入するとsetValue() 関数が呼び出されます。
最初の2つのパラメータは同じであり、3つ目は、代入された値を保持します。
e.p = "NEW"
これは次の通り出力します
NEW は Example@33a17727 の ‘p’ に代入された。
委譲される側のオブジェクトに要求される仕様は以下に説明があります。
関数やコードブロックの中で委譲プロパティを定義する事も出来ます。別にクラスのメンバである必要はありません。 以下にその例も出てきます。
標準デリゲート
Kotlin標準ライブラリでは、いくつかの有用なデリゲートのファクトリメソッドを提供します。
遅延プロパティ (lazy properties)
lazy() はラムダを引数にとり、遅延プロパティを実装するためのデリゲートとして機能する Lazy<T> のインスタンスを返す関数です。
最初のget() の呼び出しは lazy() に渡されたラムダを実行し、結果を記憶します。 それ以降、get() を呼び出すと、単に記憶された結果が返されます。
デフォルトでは、遅延プロパティの評価は 同期されます(synchronized) 。
値は1つのスレッドで計算され、すべてのスレッドから同じ値が見えます。
もし初期化デリゲートの同期が必要ではない場合は、 複数のスレッドが同時に初期化を実行できるように LazyThreadSafetyMode.PUBLICATION を lazy() 関数のパラメータとして渡します。
初期化が常に単一のスレッドで起こると確信しているなら、任意のスレッドの安全性の保証および関連するオーバーヘッドが発生しない LazyThreadSafetyMode.NONE モードを使用することができます。
このモードは一切のスレッドセーフティの保証をせず、関連するオーバーヘッドも存在しません。
Observableプロパティ
Delegates.observable() は、2つの引数を取ります。
初期値と修正のためのハンドラです。
ハンドラはプロパティに値が代入されるたびに(代入が行われた 後 に)呼び出されます。
それには3つのパラメータがあり、割り当てられているプロパティ、古い値、そして新しい値です:
もし代入に割り込んで、場合によってはそれを拒否(veto)したい場合には、
observable()の代わりにvetoable()を使うと良いでしょう。
vetoable に渡されたハンドラは、新しいプロパティ値の割り当てが行われる 前 に呼び出されます。
他のプロパティへの委譲
プロパティは、そのゲッターとセッターを他のプロパティに委譲する事が出来ます。 そのような委譲はトップレベルとクラスのプロパティで使用可能です(メンバと拡張(extension))。 委譲されるプロパティは:
- トップレベルのプロパティ
- 同じクラスのメンバや拡張(extension)プロパティ
- 他のクラスのメンバや拡張プロパティ
あるプロパティを別のプロパティに委譲するには、委譲先の名前に::の限定子(qualifier)をつけます。
例えば、this::delegateやMyClass::delegateなど。
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
この機能は例えば、プロパティのりネームを後方互換を保ちつつ行う時などに便利でしょう:
新しいプロパティを作り、古い方には@Deprecatedアノテーションをつけて、そして実装を委譲する訳です。
プロパティをマップに格納する
一般的な委譲プロパティの使用例のひとつとして、プロパティの値をマップ内に記憶するというのがあります。 これはJSONをパースしたり、他の「動的」なことをやるようなアプリケーションで頻繁に遭遇します。 このケースでは、委譲プロパティのデリゲートとしてマップのインスタンス自体を使用することができます。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
この例では、コンストラクタは、マップを取ります。
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
委譲プロパティは、このマップから文字列キーを使って値を取り出します。 この文字列キーはプロパティの名前に対応しています:
読み取り専用 Map の代わりに MutableMap を使用すると、var のプロパティに対しても動作します:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
ローカル委譲プロパティ
(Local delegated properties)
ローカル変数を委譲プロパティとして宣言する事も出来ます。 例えば、ローカル変数を遅延プロパティ(lazy)にしたり出来ます:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
(訳注:memoizedはいわゆるメモ化の事だと思われる)
memoizedFoo変数 は最初のアクセスの時だけ計算される。
someConditionが満たされなければ、この変数は一切計算されない。
プロパティを委譲するための要件
読み取り専用プロパティ(すなわち val)のために、デリゲートは、次のパラメータを取る getValue() という名前の関数を提供する必要があります。
thisRefは、プロパティの所有者 のと同じ型かその基底型でなければなりません(拡張プロパティの場合は拡張される対象の型)。propertyは、型KProperty <*>またはその基底型でなければなりません。
getValue()は、プロパティと同じ型(またはそのサブタイプ)を返さなければなりません。
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
変更可能な プロパティ ( var ) の場合、デリゲートは、さらにsetValue() という名前の関数で次のパラメータを取るものを追加で提供する必要があります:
thisRefは、プロパティの所有者 のと同じ型かその基底型でなければなりません(拡張プロパティの場合は拡張される対象の型)。propertyは、型KProperty <*>またはその基底型でなければなりません。valueはプロパティと同じ型(またはその基底型)でなければなりません。
class Resource
class Owner {
var varResource: Resource by ResourceDelegate()
}
class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}
getValue() および/または setValue() 関数は、委譲クラスのメンバ関数か、拡張関数のどちらかの形で提供することができます。
もともとはこれらの機能を提供していないオブジェクトにプロパティを委譲する必要がある場合、後者が便利です。
関数の両方を operator キーワードでマークする必要があります。
新しいクラスを作らずに無名オブジェクトでデリゲートを作る事も出来ます。
その為にはKotlinの標準ライブラリのReadOnlyPropertyとReadWritePropertyインターフェースを使います。
これらは必要なメソッドを提供しています:getValue()はReadOnlyPropertyに定義されていて、
ReadWritePropertyはそれを継承してさらにsetValue()を追加しています。
これはつまり、ReadOnlyPropertyが渡せる所にはいつでもReadWritePropertyを渡す事が出来る、という事を意味します。
fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
object : ReadWriteProperty<Any?, Resource> {
var curValue = resource
override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
curValue = value
}
}
val readOnlyResource: Resource by resourceDelegate() // ReadWriteProperty を val に使う
var readWriteResource: Resource by resourceDelegate()
委譲プロパティのトランスレーションルール
水面下では、Kotlinコンパイラはある種の委譲プロパティの場合には、補助的なプロパティを生成して、それに委譲します。
最適化のために、コンパイラは幾つかのケースでは補助的なプロパティを生成しません。 他のプロパティへの委譲の例で最適化について学べます。
例えば、プロパティpropに対しては隠しプロパティのprop$delegateが生成されて、アクセサのコードは単にこの追加のプロパティに委譲します:
class C {
var prop: Type by MyDelegate()
}
// このコードがコンパイラに生成される
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
Kotlinコンパイラはpropについての必要な全情報を提供します: 最初の引数のthisは外側のクラスCのインスタンスで、this::propはprop自身を記述するKProperty型のリフレクションオブジェクトです。
委譲プロパティが最適化されるケース
$delegate フィールドはデリゲートが以下のケースでは省略されます:
-
プロパティの参照(referenced property):
class C<Type> { private var impl: Type = ... var prop: Type by ::impl } -
名前付きオブジェクト:
object NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject -
finalな
valプロパティでバッキングフィールドがあってデフォルトのゲッターで同じモジュールにある場合:val impl: ReadOnlyProperty<Any?, String> = ... class A { val s: String by impl } -
定数式、列挙型のエントリ、
this、nullなど。 以下はthisの例:class A { operator fun getValue(thisRef: Any?, property: KProperty<*>) ... val s by this }
他のプロパティへの委譲の場合のトランスレーションルール
他のプロパティへ委譲する時は、参照先のプロパティへ直接参照するコードを生成する。
それが意味する所は、prop$delegateフィールドは生成されない、という事だ。
この最適化はメモリを節約してくれる。
以下のコードを見てみよう:
class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}
prop変数へのプロパティアクセサは、impl変数を直接実行し、
getValueとsetValue演算子を省略し、その結果KProperty参照オブジェクトは不要となる。
さきほどのコードから、コンパイラは以下のコードを生成する:
class C<Type> {
private var impl: Type = ...
var prop: Type
get() = impl
set(value) {
impl = value
}
fun getProp$delegate(): Type = impl // このメソッドはリフレクションの為だけに必要
}
委譲の提供(Providing a delegate)
provideDelegate演算子を定義すると、
プロパティの実装が委譲される対象のオブジェクトの生成のロジックを拡張出来る。
byの右側で使われるオブジェクトにprovideDelegateがメンバか拡張(extension)として定義してあると、
プロパティの委譲先インスタンス(delegate instance)を作るのに呼ばれる。
provideDelegateの考えられるユースケースの一つに、
その初期化時に対象のプロパティの一貫性をチェックするというのが挙げられる。
例えば、バインディングに先立ちプロパティの名前をチェックするには、以下のようなコードを書く事が出来る:
class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// デリゲートの作成
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
provideDelegateのパラメータはgetValueのものと同じです:
thisRefは、プロパティの所有者 のと同じ型かその基底型でなければなりません(拡張プロパティの場合は拡張される対象の型)。propertyは、型KProperty <*>またはその基底型でなければなりません。
MyUIインスタンスの作成時にその各プロパティに対してそれぞれprovideDelegateメソッドは呼ばれ、
その場で必要なバリデーションを実行する。
プロパティとデリゲートの間のバインディングを横取りするこの機能が無ければ、 同じような機能を達成する為には、 プロパティの名前を明示的に渡さないといけなくなってしまうが、 それはあんまり便利とは言えない:
// "provideDelegate"の機能無しでのプロパティ名のチェック
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// デリゲートの作成
}
生成されたコードでは、provideDelegateメソッドは補助的なプロパティ prop$delegateを初期化するために呼ばれる。
上にあるval prop: Type by MyDelegate()と宣言された時に生成されるコード(provideDelegateメソッドが無い場合)と比較せよ:
(訳注:たぶんvar prop: Type by MyDelegate()の間違いだと思う)
class C {
var prop: Type by MyDelegate()
}
// `provideDelegate`関数が使える時は、
// 以下のコードがコンパイラにより生成される
class C {
// 追加の"delegate"プロパティを作る為に"provieDelegate"を呼び出す
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
provideDelegateメソッドは補助的なプロパティの生成に影響するだけで、
ゲッターとセッターのために生成されるコードには影響を与えない事に注目して欲しい。
標準ライブラリのPropertyDelegateProviderインターフェースを使えば、
新しいクラスを作らずにデリゲートプロバイダを作成出来る:
val provider = PropertyDelegateProvider { thisRef: Any?, property ->
ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider