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

За помощь в переводе спасибо официальному блогу JetBrains на Хабре

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

  • ленивые свойства (ориг.: lazy properties): значение вычисляется один раз, при первом обращении;
  • свойства, на события об изменении которых можно подписаться (ориг.: observable properties);
  • свойства, хранимые в ассоциативном списке, а не в отдельных полях.

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

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

Их синтаксис выглядит следующим образом: val/var <имя свойства>: <Тип> by <выражение>. Выражение после 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, вызывается метод getValue() класса Delegate, причем первым параметром ей передается тот объект, у которого запрашивается свойство p, а вторым — объект-описание самого свойства p (у него можно, в частности, узнать имя свойства).

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

Этот код выведет:

Example@33a17727, спасибо за делегирование мне 'p'!

Похожим образом, когда мы обращаемся к p, вызывается метод setValue(). Два первых параметра — такие же, а третий — присваиваемое значение свойства.

e.p = "NEW"

Этот код выведет:

NEW было присвоено значению 'p' в Example@33a17727.

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

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

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

Стандартная библиотека Kotlin предоставляет несколько полезных видов делегатов.

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

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

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

Этот код выведет:

computed!
Hello
Hello

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

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

Observable свойства

Функция 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"
}

Этот код выведет:

<no name> -> first
first -> 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()
   // Уведомление: 'oldName: Int' устарело.
   // Используйте 'newName'
   myClass.oldName = 42
   println(myClass.newName) // 42
}

Хранение свойств в ассоциативном списке

Один из самых частых сценариев использования делегированных свойств заключается в хранении свойств в ассоциативном списке (map). Это полезно в “динамическом” коде, например, при работе с 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
))

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

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

Также, если вы используете MutableMap вместо read-only Map, поддерживаются изменяемые свойства var.

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 будет ложно, значение переменной не будет вычислено вовсе.

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

Для read-only свойства (например 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 неизменяемое
var readWrite: Int by resourceDelegate()

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

Под правилами преобразования (ориг.: translation rules) здесь понимаются правила, по которым компилируются делегированные свойства.

Для каждого делегированного свойства компилятор 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 reflection-объект типа 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 // Этот метод нужен только для рефлексии
}

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

С помощью определения оператора 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)
        // создание делегата
        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, и сразу совершает необходимые проверки.

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

// Проверяем имя свойства без "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)
    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