Spec-Zone.ru › Kotlin 1.6

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

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

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

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

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

Для покрытия этих (и других) случаев 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 вызывается до присвоения нового значения свойства.

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

Свойство может делегировать свой getter и setter другому свойству. Такая делегация доступна как для свойств верхнего уровня, так и для свойств класса (членов и расширений). Делегируемое свойство может быть:

  • Свойством верхнего уровня

  • Членским или расширяющим свойством того же класса

  • Членским или расширяющим свойством другого класса

Чтобы делегировать свойство другому свойству, используйте квалификатор :: в имени делегата, например, 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 завершится неудачно, переменная вообще не будет вычислена.

END_OF_DOCUMENT_MARKER

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

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

Правила трансляции при делегировании другому свойству

При делегировании другому свойству компилятор 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
Последнее изменение: 07 апреля 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