Spec-Zone.ru › Kotlin 1.8

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

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

  • Ленивые свойства: значение вычисляется только при первом обращении.

  • Наблюдаемые свойства: слушатели уведомляются об изменениях этого свойства.

  • Хранение свойств в карте вместо отдельного поля для каждого свойства.

Чтобы охватить эти (и другие) случаи, Kotlin поддерживает делегированные свойства:

class Example {
    var p: String by Delegate()
}

Синтаксис: val/var <property name>: <Type> by <expression>. Выражение после by является делегатом, потому что методы get() (и set()) свойства будут делегированы методам getValue() и setValue() его делегата. Делегаты свойств не обязаны реализовывать интерфейс, но они должны предоставлять функцию getValue() (и setValue() для var свойств).

Например:

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

При чтении из p, который делегирует экземпляру Delegate, вызывается функция getValue() из Delegate. Её первый параметр — объект, из которого вы читаете p, а второй параметр содержит описание самого свойства p (например, его имя).

val e = Example()
println(e.p)

Это выводит:

Example@33a17727, thank you for delegating 'p' to me!

Аналогично, при присваивании p, вызывается функция setValue(). Первые два параметра такие же, а третий содержит присваиваемое значение:

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

По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке, но все потоки увидят одно и то же значение. Если синхронизация инициализирующего делегата не требуется, чтобы разрешить нескольким потокам выполнять его одновременно, передайте LazyThreadSafetyMode.PUBLICATION в качестве параметра к lazy().

Если вы уверены, что инициализация всегда происходит в том же потоке, что и тот, в котором вы используете свойство, вы можете использовать LazyThreadSafetyMode.NONE. Она не несёт никаких гарантий потокобезопасности и связанных с этим накладных расходов.

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

Delegates.observable() принимает два аргумента: начальное значение и обработчик изменений.

Обработчик вызывается каждый раз, когда вы присваиваете свойству значение (после того, как присвоение выполнено). Он имеет три параметра: свойство, которому присваивается значение, старое значение и новое значение:

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

Если вы хотите перехватывать присвоения и запрещать их, используйте vetoable() вместо observable(). Обработчик, переданный в 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()
   // Notification: 'oldName: Int' is deprecated.
   // Use 'newName' instead
   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
    ))
//sampleStart
    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
//sampleEnd
}

Это также работает для свойств var , если вы используете MutableMap вместо только для чтения Map:

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

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

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

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

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

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

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

Для только для чтения свойства (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.

Вы можете создавать делегаты как анонимные объекты без создания новых классов, используя интерфейсы ReadOnlyProperty и ReadWriteProperty из стандартной библиотеки Kotlin. Они предоставляют необходимые методы: getValue() объявлен в ReadOnlyProperty; ReadWriteProperty расширяет его и добавляет setValue(). Это означает, что вы можете передать ReadWriteProperty в любое место, где ожидается ReadOnlyProperty.

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

Правила трансляции для делегированных свойств

Внутри Kotlin-компилятор генерирует вспомогательные свойства для некоторых типов делегированных свойств и затем делегирует к ним.

В целях оптимизации, компилятор не генерирует вспомогательные свойства в нескольких случаях. Узнайте об оптимизации на примере делегирования к другому свойству.

Например, для свойства prop он генерирует скрытое свойство prop$delegate, и код аксессоров просто делегирует этому дополнительному свойству:

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

Kotlin-компилятор предоставляет всю необходимую информацию о prop в аргументах: первый аргумент this ссылается на экземпляр внешнего класса C, а this::prop — это объект рефлексии типа KProperty описывающий само prop.

Оптимизированные случаи для делегированных свойств

Поле $delegate будет опущено, если делегат является:

  • Ссылочным свойством:

    class C<Type> {
        private var impl: Type = ...
        var prop: Type by ::impl
    }
    
  • Именованным объектом:

    object NamedObject {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
    }
    
    val s: String by NamedObject
    
  • Заключительным val свойством с резервным полем и значением по умолчанию для геттера в одном модуле:

    val impl: ReadOnlyProperty<Any?, String> = ...
    
    class A {
        val s: String by impl
    }
    
  • Постоянным выражением, элементом перечисления, this, null. Пример this:

    class 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 переменной вызывают переменную impl напрямую, пропуская операторы делегированного свойства getValue и setValue, и поэтому объект ссылки KProperty не нужен.

Для приведенного выше кода компилятор генерирует следующий код:

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
}

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

Определяя оператор 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)
        // 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)
}

Параметры provideDelegate такие же, как у getValue:

  • thisRef должен быть того же типа, что и, или супертипом владелец свойства (для расширяемых свойств он должен быть типом, к которому производится расширение);

  • property должен быть типа KProperty<*> или его супертипа.

Метод provideDelegate вызывается для каждого свойства во время создания экземпляра MyUI, и он выполняет необходимую валидацию сразу.

Без этой возможности перехвата привязки между свойством и его делегатом, для достижения такой же функциональности вам пришлось бы явно передавать имя свойства, что не очень удобно:

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

В сгенерированном коде метод provideDelegate вызывается для инициализации вспомогательного свойства prop$delegate. Сравните сгенерированный код для объявления свойства val prop: Type by MyDelegate() с сгенерированным кодом выше (когда метод provideDelegate отсутствует):

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

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

С интерфейсом PropertyDelegateProvider из стандартной библиотеки вы можете создавать поставщиков делегатов без создания новых классов.

val provider = PropertyDelegateProvider { thisRef: Any?, property ->
    ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider
Последнее изменение: 10 января 2023 г.
Делегирование Псевдонимы типов

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/delegated-properties.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API