Spec-Zone.ru › Kotlin 1.7

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

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

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

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

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

Для покрытия этих (и других) случаев Kotlin поддерживает делегированные свойства:

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

Синтаксис: val/var <property name>: <Type> by <expression>. Выражение после by — это делегат, потому что обращения к свойству (и 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(): ReadWriteProperty<Any?, Int> =
    object : ReadWriteProperty<Any?, Int> {
        var curValue = 0 
        override fun getValue(thisRef: Any?, property: KProperty<*>): Int = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            curValue = value
        }
    }

val readOnly: Int by resourceDelegate()  // ReadWriteProperty as val
var readWrite: Int 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 свойство с вспомогательным полем и по умолчанию getter в том же модуле:

    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 влияет только на создание вспомогательного свойства и не влияет на код, сгенерированный для getter или setter.

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

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

© 2010–2022 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