Spec-Zone.ru › Kotlin 2

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

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

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

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

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

Для поддержки этих и других случаев 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"
}

Если вы хотите перехватывать присваивания и отклонять их, используйте вместо observable() функцию vetoable(). Обработчик, переданный в 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, если вместо доступного только для чтения Map использовать MutableMap:

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(). Это означает, что вместо ожидаемого ReadOnlyProperty можно передать ReadWriteProperty.

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 с модификатором final, полем для хранения значения и геттером по умолчанию в том же модуле:

    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
25 сентября 2024 г.
СвойстваБезопасность относительно null

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