1.1.50.19. fejezet, Delegált tulajsonságok
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.") } } class Example { var p: String by Delegate() } fun main() { val e = Example() println(e.p) e.p = "NEW" } // Example@49c2faae, thank you for delegating 'p' to me! // NEW has been assigned to 'p' in Example@49c2faae.
Standard delegáltak
Lazy tulajdonságok
val lazyValue: String by lazy { println("computed!") "Hello" } fun main() { println(lazyValue) println(lazyValue) } // computed! // Hello // Hello
Alapértelmezés szerint a lazy tulajdonságok kiértékelése szinkronizálva van: az értéket csak egy szálban számítja ki, de minden szál ugyanazt az értéket fogja látni. Ha a delegált inicializálását nem szükséges szinkronizálni, hogy egyszerre több szál is végrehajthassa azt, adjuk át a lazy() függvénynek a LazyThreadSafetyMode.PUBLICATION paramétert.
Ha biztosak akarunk lenni abban, hogy az inicializálás mindig ugyanabban a szálban fog történni, mint ahol a tulajdonságot használjuk, akkor alkalmazzuk a LazyThreadSafetyMode.NONE paramétert.
Megfigyelhető tulajdonságok
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" }
Ha el szeretnénk elfogni és meg szeretnénk vétózni a hozzárendeléseket, használjuk a vetoable() metódust.
Delegálás másik tulajdonságra
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
Ez például akkor lehet hasznos, ha visszamenőlegesen kompatibilis módon szeretne átnevezni egy tulajdonságot: új tulajdon bevezetése
class MyClass { var newName: Int = 0 @Deprecated("Use 'newName' instead", ReplaceWith("newName")) var oldName: Int by this::newName } fun main() { val myClass = MyClass() // Notification: 'oldName: Int' is deprecated. // Use 'newName' instead myClass.oldName = 42 println(myClass.newName) // 42 }
Tulajdonság tárolása Map-ban
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 )) println(user.name) // Prints "John Doe" println(user.age) // Prints 25
Ez működik var tulajdonságokra is.
class User(val map: MutableMap<String, Any?>) { var name: String by map var age: Int by map } val map = mutableMapOf<String, Any?>( "name" to "John Doe", "age" to 25 ) val user = User(map) fun main() { println(user.name) // Prints "John Doe" println(user.age) // Prints 25 user.name = "Zoltan Papp" println(map.get("name")) // Prints "Zoltan Papp" }
Lokális delegált tulajdonságok
fun example(computeFoo: () -> Foo) { val memoizedFoo by lazy(computeFoo) if (someCondition && memoizedFoo.isValid()) { memoizedFoo.doSomething() } }
A memoizedFoo változó csak az első elérésekor értékelődik ki. Ha a someCondition false, a változó egyáltalán nem értékelődik ki.
Tulajdonságdelegálási követelmények
- thisRef - Ugyanolyan típusúnak vagy szupertípusnak kell lennie, mint a tulajdonság tulajdonosának (kiterjesztési tulajdonságok esetén a bővített típusnak kell lennie).
- property - KProperty<*> típusúnak vagy szupertípusnak kell lennie
- getValue() - ugyanazt a típust kell visszaadnia, mint a tulajdonságnak (vagy annak altípusának)
class Resource class Owner { val valResource: Resource by ResourceDelegate() } class ResourceDelegate { operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource { return Resource() } }
Módosítható (var) tulajdonságoknál:
- thisRef - Ugyanolyan típusúnak vagy szupertípusnak kell lennie, mint a tulajdonság tulajdonosának (kiterjesztési tulajdonságok esetén a bővített típusnak kell lennie).
- property - KProperty<*> típusúnak vagy szupertípusnak kell lennie
- value - ugyanolyan típusúnak kell lennie, mint a tulajdonságnak (vagy annak szupertípusának).
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 } } }
A getValue és/vagy setValue függvények a delegált osztály tagfüggvényeiként vagy kiterjesztett függvényeiként is megadhatók.
A delegáltakat névtelen objektumként új osztályok létrehozása nélkül is létrehozhatjuk a ReadOnlyProperty és ReadWriteProperty interfészekkel.
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 as val var readWriteResource: Resource by resourceDelegate()
A delegált tulajdonságok fordítási szabályai
Például a prop tulajdonsághoz létrehozza a prop$delegate rejtett tulajdonságot, és a hozzáférési kódok egyszerűen delegálják ezt a további tulajdonságot:
class C { var prop: Type by MyDelegate() } // this code is generated by the compiler instead: 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) }
Delegált tulajdonságokra optimalizált esetek
A $delegate mező generálása nem történik meg ha a delegált :
- Egy hivatkozott tulajdonság
class C<Type> { private var impl: Type = ... var prop: Type by ::impl }
- Egy megnevezett objektum
object NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject
- Egy val csak olvasható tulajdonság egy háttérmezővel és egy alapértelmezett getterrel ugyanabban a modulban:
val impl: ReadOnlyProperty<Any?, String> = ... class A { val s: String by impl }
- Konstans kifejezés, felsorolás bejegyzés, this, null:
class A { operator fun getValue(thisRef: Any?, property: KProperty<*>) ... val s by this }
Fordítási szabályok másik tulajdonságra történő delegálás esetén
Ha egy másik tulajdonságra delegál, a Kotlin fordító közvetlen hozzáférést biztosít a hivatkozott tulajdonsághoz. Ez azt jelenti, hogy a fordító nem generálja a prop$delegate mezőt. Ez az optimalizálás segít a memória megtakarításban.
class C<Type> { private var impl: Type = ... var prop: Type by ::impl }
A fenti kódhoz a fordító a következő kódot generálja:
class C<Type> { private var impl: Type = ... var prop: Type get() = impl set(value) { impl = value } fun getProp$delegate(): Type = impl // This method is needed only for reflection }
Delegált biztosítása
A provideDelegate operátor definiálásával kiterjeszthetjük annak az objektumnak a létrehozására szolgáló logikát, amelyhez a tulajdonság implementáció delegálva lett. Ha az objektum ami a by jobb oldalán szerepel definiál egy provideDelegate-ot mint egy tagot vagy bővítményé funkciót, ez a funkció meghívódik hogy létrehozza a delegált tulajdonság példányát.
Ha például kötés előtt ellenőrizni szeretné a tulajdonság nevét, írhatunk valami ilyesmit:
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) // create delegate 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) }
A provideDelegate paraméterei hasonlóak a getValue-hoz:
- thisRef - ugyanolyan típusúnak vagy szupertípusnak kell lennie, mint az ingatlan tulajdonosának (kiterjesztési tulajdonságok esetén a bővített típusnak kell lennie);
- property - KProperty<*> típusúnak vagy szupertípusúnak kell lennie.
A provideDelegateMyUI metódust a rendszer minden tulajdonsághoz meghívja a MyUI példány létrehozása során, és azonnal elvégzi a szükséges érvényesítést.
A tulajdonság és a delegált közötti kötés elfogásának képessége nélkül ugyanannak a funkciónak az eléréséhez explicit módon kell átadnia a tulajdonság nevét, ami nem túl kényelmes:
// Checking the property name without "provideDelegate" functionality 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) // create delegate }
A generált kódban a provideDelegate metódus meghívásra kerül a kiegészítő prop$delegate tulajdonság inicializálására.
Hasonlítsuk össze a val prop: Type by MyDelegate() tulajdonság deklarációkhoz generált kódot a fenti generált kóddal (ha a provideDelegate módszer nincs jelen):
class C { var prop: Type by MyDelegate() } // this code is generated by the compiler // when the 'provideDelegate' function is available: class C { // calling "provideDelegate" to create the additional "delegate" property 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) }
Figyeljük meg, hogy a provideDelegate metódus csak a kiegészítő tulajdonság létrehozására van hatással, és nincs hatással a getter vagy a setter számára létrehozott kódra.
A PropertyDelegateProvider szabványos könyvtár interfészével új osztályok létrehozása nélkül hozhatunk létre deletált szolgáltatókat.
val provider = PropertyDelegateProvider { thisRef: Any?, property -> ReadOnlyProperty<Any?, Int> {_, property -> 42 } } val delegate: Int by provider
- A hozzászóláshoz be kell jelentkezni