委譲プロパティ
いくつかの一般的な種類のプロパティでは、必要になるたびに手動で実装することもできますが、一度だけ実装してライブラリに追加し、後で再利用できるようにする方が便利です。例えば:
- 遅延(Lazy)プロパティ: 最初のアクセス時にのみ値が計算されます。
- 観察可能(Observable)プロパティ: このプロパティの変更がリスナーに通知されます。
- プロパティごとに個別のフィールドを用意するのではなく、マップ(map)にプロパティを保存する場合。
これら(およびその他の)ケースをカバーするために、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, 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() 関数が呼び出されます。その第1パラメータは 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.委譲されるオブジェクトの要件に関する仕様は、以下にあります。
委譲プロパティは関数内やコードブロック内で宣言することもできます。必ずしもクラスのメンバである必要はありません。 例は以下にあります。
標準の委譲
Kotlin 標準ライブラリは、いくつかの便利な種類の委譲のためのファクトリメソッドを提供しています。
遅延プロパティ
lazy() はラムダを受け取り、遅延プロパティを実装するための委譲として機能する Lazy<T> のインスタンスを返す関数です。 get() の最初の呼び出しで lazy() に渡されたラムダが実行され、その結果が記憶されます。 その後の get() の呼び出しでは、単に記憶された結果を返します。
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}デフォルトでは、遅延プロパティの評価は同期(synchronized)されます。値は1つのスレッドでのみ計算されますが、すべてのスレッドが同じ値を参照することになります。初期化の委譲の同期が不要で、複数のスレッドが同時に実行できるようにしたい場合は、lazy() のパラメータとして LazyThreadSafetyMode.PUBLICATION を渡してください。
初期化が常にプロパティを使用するのと同じスレッドで行われることが確実な場合は、LazyThreadSafetyMode.NONE を使用できます。これは、スレッドセーフの保証やそれに関連するオーバーヘッドを一切伴いません。
観察可能プロパティ
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() {
val user = User()
user.name = "first"
user.name = "second"
}代入をインターセプトして拒否(veto)したい場合は、observable() の代わりに vetoable() を使用してください。 vetoable に渡されたハンドラは、新しいプロパティ値の代入が実行される前に呼び出されます。
別のプロパティへの委譲
プロパティは、そのゲッターとセッターを別のプロパティに委譲できます。このような委譲は、トップレベルおよびクラスのプロパティ(メンバおよび拡張)の両方で利用可能です。委譲先のプロパティは以下のいずれかになります:
- トップレベルプロパティ
- 同じクラスのメンバまたは拡張プロパティ
- 別のクラスのメンバまたは拡張プロパティ
プロパティを別のプロパティに委譲するには、委譲先の名称に :: 修飾子を使用します。例えば、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 アノテーションを付けて、その実装を委譲します。
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// 通知: 'oldName: Int' は非推奨です。
// 代わりに 'newName' を使用してください。
myClass.oldName = 42
println(myClass.newName) // 42
}マップへのプロパティ保存
一般的なユースケースの一つは、プロパティの値をマップに保存することです。 これは、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
))委譲プロパティは、プロパティ名に関連付けられた文字列キーを介して、このマップから値を取得します。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
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
}ローカル委譲プロパティ
ローカル変数を委譲プロパティとして宣言できます。 例えば、ローカル変数を遅延初期化(lazy)にすることができます:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}memoizedFoo 変数は、最初のアクセス時にのみ計算されます。 someCondition が false の場合、この変数は一切計算されません。
プロパティ委譲の要件
読み取り専用プロパティ(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 に関する必要なすべての情報を提供します。第1引数の this は外側のクラス C のインスタンスを指し、this::prop は prop 自体を記述する KProperty 型のリフレクションオブジェクトです。
委譲プロパティの最適化されたケース
委譲先が以下の場合、$delegate フィールドは省略されます:
参照されたプロパティ:
kotlinclass C<Type> { private var impl: Type = ... var prop: Type by ::impl }名前付きオブジェクト(Named object):
kotlinobject NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject同じモジュール内にある、バッキングフィールドとデフォルトのゲッターを持つ final な
valプロパティ:kotlinval impl: ReadOnlyProperty<Any?, String> = ... class A { val s: String by impl }定数式、列挙型のエントリ、
this、null。thisの例:kotlinclass A { operator fun getValue(thisRef: Any?, property: KProperty<*>) ... val s by this }
別のプロパティに委譲する場合の変換ルール
別のプロパティに委譲する場合、Kotlin コンパイラは参照されたプロパティへの直接アクセスを生成します。 これは、コンパイラが prop$delegate フィールドを生成しないことを意味します。この最適化により、メモリを節約できます。
例えば、次のコードを考えてみましょう:
class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}prop 変数のプロパティアクセサは、委譲プロパティの getValue および setValue 演算子をスキップして impl 変数を直接呼び出すため、KProperty 参照オブジェクトは不要になります。
上記のコードに対して、コンパイラは以下のコードを生成します:
class C<Type> {
private var impl: Type = ...
var prop: Type
get() = impl
set(value) {
impl = value
}
fun getProp$delegate(): Type = impl // このメソッドはリフレクションのためにのみ必要です
}委譲の提供
provideDelegate 演算子を定義することで、プロパティの実装が委譲されるオブジェクトを作成するためのロジックを拡張できます。by の右側で使用されるオブジェクトがメンバ関数または拡張関数として provideDelegate を定義している場合、その関数が呼び出されてプロパティ委譲インスタンスが作成されます。
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<*>型またはそのスーパータイプである必要があります。
provideDelegate メソッドは MyUI インスタンスの作成中に各プロパティに対して呼び出され、即座に必要なバリデーションを実行します。
プロパティとその委譲の間のバインドをインターセプトするこの機能がない場合、同じ機能を実現するにはプロパティ名を明示的に渡す必要があり、あまり便利ではありません:
// "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)
// 委譲を作成
}生成されたコードでは、補助的な prop$delegate プロパティを初期化するために provideDelegate メソッドが呼び出されます。プロパティ宣言 val prop: Type by MyDelegate() に対して生成されたコードを、上記(provideDelegate メソッドが存在しない場合)の生成コードと比較してください:
class C {
var prop: Type by MyDelegate()
}
// 'provideDelegate' 関数が利用可能な場合に
// コンパイラによって生成されるコード:
class C {
// 追加の "delegate" プロパティを作成するために "provideDelegate" を呼び出す
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