Делегирование в Kotlin

Шаблон делегированияШаблон делегирования — это фундаментальная техника в шаблонах проектирования программного обеспечения. В шаблоне делегирования два объекта участвуют в обработке одного и того же запроса. Объект, который получает запрос, делегирует запрос другому объекту для обработки.

Kotlin напрямую поддерживает шаблон делегирования, делая его более элегантным и лаконичным. Kotlin реализует делегирование через ключевое слово `by`.


Делегирование классов

Делегирование классов означает, что методы, определённые в классе, фактически реализуются путём вызова методов объекта другого класса.

В следующем примере производный класс `Derived` наследует все методы интерфейса `Base` и делегирует входящему объекту класса `Base` выполнение этих методов.

// 创建接口
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
}

В объявлении `Derived` конструкция `by` означает, что `b` хранится внутри экземпляра объекта `Derived`, а компилятор сгенерирует все методы, унаследованные от интерфейса `Base`, и перенаправит вызовы к `b`.


Делегирование свойств

Делегирование свойств означает, что значение свойства класса задаётся не напрямую в классе, а поручается классу-делегату, что позволяет обеспечить единообразное управление свойствами класса.

Синтаксис делегирования свойств:

val/var <属性名>: <类型> by <表达式>
  • var/val: тип свойства (изменяемое/только для чтения)
  • Имя свойства: имя свойства
  • Тип: тип данных свойства
  • Выражение: класс делегата

Выражение после ключевого слова `by` является делегатом. Метод `get()` свойства (и метод `set()`) будет делегирован методам `getValue()` и `setValue()` этого объекта. Делегирование свойства не требует реализации какого-либо интерфейса, но должно предоставлять функцию `getValue()` (для свойств `var` также требуется функция `setValue()`).

Определение класса-делегата

Этот класс должен содержать методы `getValue()` и `setValue()`. Параметр `thisRef` — это объект класса, выполняющего делегирование, а `prop` — объект делегируемого свойства.

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

Результат вывода:

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

Стандартные делегаты

Стандартная библиотека Kotlin содержит множество встроенных фабричных методов для реализации делегирования свойств.

Ленивые свойства

`lazy()` — это функция, которая принимает лямбда-выражение в качестве параметра и возвращает экземпляр `Lazy<T>`. Возвращённый экземпляр можно использовать как делегат для реализации ленивых свойств: первый вызов `get()` выполняет лямбда-выражение, переданное в `lazy()`, и записывает результат; последующие вызовы `get()` просто возвращают записанный результат.

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

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

Вывод после выполнения:

computed!
Hello
Hello

Наблюдаемые свойства

`observable` можно использовать для реализации шаблона наблюдателя.

Функция `Delegates.observable()` принимает два параметра: первый — начальное значение, а второй — обработчик события изменения значения свойства.

После присваивания свойства выполняется обработчик события. У него три параметра: свойство, которому присваивается значение, старое значение и новое значение:

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 = "第二次赋值"
}

Вывод после выполнения:

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

Хранение свойств в Map

Распространённый сценарий использования — хранение значений свойств в map. Это часто встречается в приложениях, которые парсят JSON или занимаются другими «динамическими» вещами. В этом случае можно использовать сам экземпляр map в качестве делегата для реализации делегированных свойств.

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

Вывод после выполнения:

Example
www.example.com

Если вы используете свойство `var`, нужно заменить `Map` на `MutableMap`:

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)

}

Вывод после выполнения:

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

Не null

`notNull` подходит для ситуаций, когда значение свойства нельзя определить на этапе инициализации.

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

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

Следует отметить, что если обратиться к свойству до его присваивания, будет выброшено исключение.


Локальные делегированные свойства

Можно объявлять локальные переменные как делегированные свойства. Например, можно сделать локальную переменную лениво инициализируемой:

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

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

Переменная `memoizedFoo` вычисляется только при первом доступе. Если `someCondition` не выполняется, переменная вообще не будет вычислена.


Требования к делегированию свойств

Для свойств только для чтения (т.е. свойств `val`) их делегат должен предоставлять функцию с именем `getValue()`. Эта функция принимает следующие параметры:

  • `thisRef` — должен иметь тот же тип, что и тип владельца свойства (для свойств-расширений — расширяемый тип), или его супертип.
  • `property` — должен иметь тип `KProperty<*>` или его супертип.

Эта функция должна возвращать тот же тип, что и свойство (или его подтип).

Для изменяемого свойства (т.е. свойства `var`), в дополнение к функции `getValue()`, его делегат также должен предоставлять функцию с именем `setValue()`. Эта функция принимает следующие параметры:

  • `property` — должен иметь тип `KProperty<*>` или его супертип.
  • `new value` — должен иметь тот же тип, что и свойство, или его супертип.

Правила преобразования

За каждой реализацией делегированного свойства компилятор 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` — это объект рефлексии типа `KProperty`, который описывает сам `prop`.


Предоставление делегата

Определив оператор `provideDelegate`, вы можете расширить логику создания объекта, которому делегируется реализация свойства. Если объект, используемый справа от `by`, определяет `provideDelegate` как функцию-член или функцию-расширение, эта функция будет вызвана для создания экземпляра делегата свойства.

Один из возможных вариантов использования `provideDelegate` — проверка согласованности свойства при его создании (а не только в его геттере или сеттере).

Например, если вы хотите проверить имя свойства перед привязкой, можно написать:

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

Параметры `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` не существует):

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

Обратите внимание, что метод `provideDelegate` влияет только на создание вспомогательного свойства и не влияет на код, сгенерированный для геттера или сеттера.

Другие расширения