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

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

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

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

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

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

Их синтаксис выглядит следующим образом: val/var <имя свойства>: <Тип> by <выражение>. Выражение после byделегат: обращения (get(), set()) к свойству будут обрабатываться этим выражением. Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были методы get() и set() с определённой сигнатурой:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, спасибо за делегирование мне '${property.name}'!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value было присвоено значению '${property.name} в $thisRef.'")
    }
}

Когда мы читаем значение свойства p, вызывается метод getValue() класса Delegate, причем первым параметром ей передается тот объект, у которого запрашивается свойство p, а вторым — объект-описание самого свойства p (у него можно, в частности, узнать имя свойства). Например:

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

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

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

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

e.p = "NEW"

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

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

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

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

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

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

Ленивые свойства (lazy properties)

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

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

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

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

computed!
Hello
Hello

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

Observable свойства

Функция Delegates.observable() принимает два аргумента: начальное значение свойства и обработчик (лямбда), который вызывается при изменении свойства. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение. Если Вам нужно иметь возможность запретить присваивание некоторых значений, используйте функцию vetoable() вместо observable().

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

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

<no name> -> first
first -> second

Если Вам нужно иметь возможность запретить присваивание некоторых значений, используйте функцию vetoable() вместо observable().

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

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

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

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

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

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

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

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

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

Здесь приведены требования к объектам-делегатам.

Для read-only свойства (например val), делегат должен предоставлять функцию getValue, которая принимает следующие параметры:

  • thisRef — должен иметь такой же тип, или быть наследником типа хозяина свойства (для расширений — тип, который расширяется)
  • property — должен быть типа KProperty<*> или его родительского типа. Это функция должна возвращать значение того же типа, что и свойство (или его родительского типа).

Для изменяемого свойства (var), делегат должен дополнительно предоставлять функцию setValue, которая принимает следующие параметры:

  • thisRef — то же что и у getValue(),
  • property — то же что и у getValue(),
  • new value — должен быть того же типа, что и свойство (или его родительского типа).

Функции getValue() и/или setValue() могут быть предоставлены либо как члены класса-делегата, либо как его расширения. Последнее полезно когда вам нужно делегировать свойство объекту, который изначально не имеет этих функций. Обе эти функции должны быть отмечены с помощью ключевого слова operator.

Эти интерфейсы объявлены в стандартной библиотеке Kotlin:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

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.

Заметьте, что синтаксис this::prop для обращения к bound callable reference напрямую в коде программы доступен только с Kotlin версии 1.1

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

Примечание: Предоставление делегата доступно в Kotlin начиная с версии 1.1

С помощью определения оператора provideDelegate вы можете расширить логику создания объекта, которому будет делегировано свойство. Если объект используется справа от by определяет provideDelegate как член или как расширение, эта функция будет вызвана для создания экземпляра делегата.

Один из возможных юзкейсов provideDelegate — это проверка состояния свойства при его создании.

Например, если вы хотите проверить имя свойства перед связыванием, вы можете написать что-то вроде:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // создание делегата
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
    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() со сгенерированным кодом из Transaction Rules (когда provideDelegate не представлен):

class C {
    var prop: Type by MyDelegate()
}

// этот код будет сгенерирован компилятором 
// когда функция 'provideDelegate' доступна:
class C {
    // вызываем "provideDelegate" для создания вспомогательного свойства "delegate"
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

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