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

За помощь в переводе спасибо официальному блогу 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, 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() в качестве аргумента, и запоминает полученное значение, а последующие вызовы просто возвращают вычисленное значение.

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

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

По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке выполнения, и все остальные потоки могут видеть одно и то же значение. Если синхронизация не требуется, передайте 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"
}

Если вам нужно перехватывать присваивания и отклонять их, используйте функцию 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
))

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

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(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 как val
var readWriteResource: Resource 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.

Оптимизированные случаи для делегированных свойств

Поле $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 с backing field и стандартным геттером в том же модуле:
  val impl: ReadOnlyProperty<Any?, String> = ...

  class A {
      val s: String by impl
  }
  • константное выражение, элемент enum, 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 // Этот метод нужен только для рефлексии
}

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

С помощью определения оператора 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