Kotlin-Delegation

DelegationsmusterDas Delegationsmuster ist eine grundlegende Technik in Software-Entwurfsmustern. Beim Delegationsmuster sind zwei Objekte an der Bearbeitung derselben Anfrage beteiligt. Das Objekt, das die Anfrage empfängt, delegiert die Anfrage zur Verarbeitung an ein anderes Objekt.

Kotlin unterstützt das Delegationsmuster direkt und macht es eleganter und prägnanter. Kotlin implementiert Delegation durch das Schlüsselwort `by`.


Klassendelegation

Klassendelegation bedeutet, dass die in einer Klasse definierten Methoden tatsächlich durch Aufrufen der Methoden eines Objekts einer anderen Klasse implementiert werden.

Im folgenden Beispiel erbt die abgeleitete Klasse `Derived` alle Methoden der Schnittstelle `Base` und delegiert an ein übergebenes Objekt der Klasse `Base`, um diese Methoden auszuführen.

// 创建接口
interface Base {   
    fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() // 输出 10
}

In der Deklaration von `Derived` bedeutet die `by`-Klausel, dass `b` innerhalb der Objektinstanz von `Derived` gespeichert wird, und der Compiler generiert alle von der Schnittstelle `Base` geerbten Methoden und leitet die Aufrufe an `b` weiter.


Eigenschaftsdelegation

Eigenschaftsdelegation bedeutet, dass ein Eigenschaftswert einer Klasse nicht direkt in der Klasse definiert wird, sondern einer Delegatenklasse anvertraut wird, wodurch eine einheitliche Verwaltung der Eigenschaften der Klasse erreicht wird.

Syntax der Eigenschaftsdelegation:

val/var <属性名>: <类型> by <表达式>
  • var/val: Eigenschaftstyp (veränderlich/schreibgeschützt)
  • Eigenschaftsname: der Name der Eigenschaft
  • Typ: der Datentyp der Eigenschaft
  • Ausdruck: die Delegatklasse

Der Ausdruck nach dem Schlüsselwort `by` ist das Delegat. Die `get()`-Methode der Eigenschaft (und die `set()`-Methode) wird an die `getValue()`- und `setValue()`-Methoden dieses Objekts delegiert. Die Eigenschaftsdelegation muss keine Schnittstelle implementieren, aber sie muss die Funktion `getValue()` bereitstellen (für `var`-Eigenschaften ist zusätzlich eine Funktion `setValue()` erforderlich).

Definieren einer delegierten Klasse

Diese Klasse muss die Methoden `getValue()` und `setValue()` enthalten. Der Parameter `thisRef` ist das Objekt der Klasse, die die Delegation durchführt, und `prop` ist das Objekt der Eigenschaft, die delegiert wird.

import kotlin.reflect.KProperty
// 定义包含属性委托的类
class Example {
    var p: String by Delegate()
}

// 委托的类
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 属性赋值为 $value")
    }
}
fun main(args: Array<String>) {
    val e = Example()
    println(e.p)     // 访问该属性,调用 getValue() 函数

    e.p = "Example"   // 调用 setValue() 函数
    println(e.p)
}

Das Ausgabeergebnis ist:

Example@433c675d, 这里委托了 p 属性
Example@433c675d 的 p 属性赋值为 Example
Example@433c675d, 这里委托了 p 属性

Standard-Delegaten

Die Kotlin-Standardbibliothek bietet viele integrierte Factory-Methoden zur Implementierung der Eigenschaftsdelegation.

Lazy-Eigenschaften

`lazy()` ist eine Funktion, die einen Lambda-Ausdruck als Parameter akzeptiert und eine Instanz von `Lazy<T>` zurückgibt. Die zurückgegebene Instanz kann als Delegat verwendet werden, um Lazy-Eigenschaften zu implementieren: Der erste Aufruf von `get()` führt den an `lazy()` übergebenen Lambda-Ausdruck aus und speichert das Ergebnis; nachfolgende Aufrufe von `get()` geben einfach das gespeicherte Ergebnis zurück.

val lazyValue: String by lazy {
    println("computed!")     // 第一次调用输出,第二次调用不执行
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)   // 第一次执行,执行两次输出表达式
    println(lazyValue)   // 第二次执行,只输出返回值
}

Die Ausgabe nach der Ausführung ist:

computed!
Hello
Hello

Beobachtbare Eigenschaften

`observable` kann verwendet werden, um das Observer-Muster zu implementieren.

Die Funktion `Delegates.observable()` akzeptiert zwei Parameter: Der erste ist der Anfangswert, und der zweite ist der Handler für das Ereignis der Wertänderung der Eigenschaft.

Nachdem die Eigenschaft zugewiesen wurde, wird der Ereignishandler ausgeführt. Er hat drei Parameter: die zugewiesene Eigenschaft, den alten Wert und den neuen Wert:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("初始值") {
        prop, old, new ->
        println("旧值:$old -> 新值:$new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "第一次赋值"
    user.name = "第二次赋值"
}

Die Ausgabe nach der Ausführung ist:

Old value: initial value -> New value: first assignment  
Old value: first assignment -> New value: second assignment

Speichern von Eigenschaften in einer Map

Ein häufiger Anwendungsfall ist das Speichern von Eigenschaftswerten in einer Map. Dies tritt oft in Anwendungen auf, die JSON parsen oder andere "dynamische" Dinge tun. In diesem Fall können Sie die Map-Instanz selbst als Delegate verwenden, um delegierte Eigenschaften zu implementieren.

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val url: String  by map
}

fun main(args: Array<String>) {
    // 构造函数接受一个映射参数
    val site = Site(mapOf(
        "name" to "Example",
        "url"  to "www.example.com"
    ))
    
    // 读取映射值
    println(site.name)
    println(site.url)
}

Die Ausgabe nach der Ausführung ist:

Example
www.example.com

Wenn Sie eine `var`-Eigenschaft verwenden, müssen Sie `Map` durch `MutableMap` ersetzen:

class Site(val map: MutableMap<String, Any?>) {
    val name: String by map
    val url: String by map
}

fun main(args: Array<String>) {

    var map:MutableMap<String, Any?> = mutableMapOf(
            "name" to "Example",
            "url" to "www.example.com"
    )

    val site = Site(map)

    println(site.name)
    println(site.url)

    println("--------------")
    map.put("name", "Google")
    map.put("url", "www.google.com")

    println(site.name)
    println(site.url)

}

Die Ausgabe nach der Ausführung ist:

Example
www.example.com
--------------
Google
www.google.com

Nicht Null

`notNull` eignet sich für Situationen, in denen der Eigenschaftswert während der Initialisierungsphase nicht bestimmt werden kann.

class Foo {
    var notNullBar: String by Delegates.notNull<String>()
}

foo.notNullBar = "bar"
println(foo.notNullBar)

Es ist zu beachten, dass eine Ausnahme ausgelöst wird, wenn auf die Eigenschaft vor der Zuweisung zugegriffen wird.


Lokale delegierte Eigenschaften

Sie können lokale Variablen als delegierte Eigenschaften deklarieren. Zum Beispiel können Sie eine lokale Variable verzögert initialisieren:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

Die Variable `memoizedFoo` wird nur beim ersten Zugriff berechnet. Wenn `someCondition` fehlschlägt, wird die Variable überhaupt nicht berechnet.


Anforderungen an die Delegation von Eigenschaften

Für schreibgeschützte Eigenschaften (d. h. `val`-Eigenschaften) muss ihr Delegat eine Funktion namens `getValue()` bereitstellen. Diese Funktion akzeptiert die folgenden Parameter:

  • `thisRef` — muss denselben Typ wie der Eigentümertyp haben (bei Erweiterungseigenschaften der erweiterte Typ) oder ein Supertyp davon sein.
  • `property` — muss vom Typ `KProperty<*>` oder ein Supertyp davon sein.

Diese Funktion muss denselben Typ wie die Eigenschaft (oder einen Subtyp davon) zurückgeben.

Für eine veränderbare Eigenschaft (d. h. eine `var`-Eigenschaft) muss ihr Delegat zusätzlich zur Funktion `getValue()` auch eine Funktion namens `setValue()` bereitstellen. Diese Funktion akzeptiert die folgenden Parameter:

  • `property` — muss vom Typ `KProperty<*>` oder ein Supertyp davon sein.
  • `new value` — muss denselben Typ wie die Eigenschaft oder einen Supertyp davon haben.

Übersetzungsregeln

Hinter jeder delegierten Eigenschaftsimplementierung generiert der Kotlin-Compiler eine Hilfseigenschaft und delegiert an sie. Beispielsweise wird für die Eigenschaft `prop` eine versteckte Eigenschaft `prop$delegate` generiert, und der Accessor-Code delegiert einfach an diese zusätzliche Eigenschaft:

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)
}

Der Kotlin-Compiler stellt alle notwendigen Informationen über `prop` in den Parametern bereit: Der erste Parameter `this` verweist auf eine Instanz der äußeren Klasse `C`, und `this::prop` ist ein Reflexionsobjekt vom Typ `KProperty`, das `prop` selbst beschreibt.


Bereitstellen eines Delegaten

Indem Sie den Operator `provideDelegate` definieren, können Sie die Logik zum Erstellen des Objekts erweitern, an das die Eigenschaftsimplementierung delegiert wird. Wenn das auf der rechten Seite von `by` verwendete Objekt `provideDelegate` als Member- oder Erweiterungsfunktion definiert, wird diese Funktion aufgerufen, um die Delegateninstanz der Eigenschaft zu erstellen.

Ein möglicher Anwendungsfall für `provideDelegate` ist die Überprüfung der Konsistenz einer Eigenschaft bei ihrer Erstellung (nicht nur in ihrem Getter oder Setter).

Wenn Sie beispielsweise den Eigenschaftsnamen vor der Bindung überprüfen möchten, können Sie Folgendes schreiben:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 创建委托
    }

    private fun checkProperty(thisRef: MyUI, name: String) { …… }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

Die Parameter von `provideDelegate` sind dieselben wie die von `getValue`:

  • `thisRef` — muss denselben Typ wie der Eigentümertyp haben (bei Erweiterungseigenschaften der erweiterte Typ) oder ein Supertyp davon sein.
  • `property` — muss vom Typ `KProperty<*>` oder ein Supertyp davon sein.

Während der Erstellung einer `MyUI`-Instanz wird die Methode `provideDelegate` für jede Eigenschaft aufgerufen und die erforderliche Validierung sofort ausgeführt.

Ohne diese Möglichkeit, die Bindung zwischen der Eigenschaft und ihrem Delegaten abzufangen, müssten Sie den Eigenschaftsnamen explizit übergeben, um dieselbe Funktionalität zu erreichen, was nicht sehr praktisch ist:

// 检查属性名称而不使用“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)
   // 创建委托
}

Im generierten Code wird die Methode `provideDelegate` aufgerufen, um die Hilfseigenschaft `prop$delegate` zu initialisieren. Vergleichen Sie den für die Eigenschaftsdeklaration `val prop: Type by MyDelegate()` generierten Code mit dem oben generierten Code (wenn die Methode `provideDelegate` nicht existiert):

class C {
    var prop: Type by MyDelegate()
}

// 这段代码是当“provideDelegate”功能可用时
// 由编译器生成的代码:
class C {
    // 调用“provideDelegate”来创建额外的“delegate”属性
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

Beachten Sie, dass die Methode `provideDelegate` nur die Erstellung der Hilfseigenschaft beeinflusst und nicht den für den Getter oder Setter generierten Code.

Andere Erweiterungen