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