Делегирование в Kotlin
В Kotlin делегирование как особенность языка реализуется на 2 уровнях: это реализация интерфейса и делегированные свойства. Ниже мы рассмотрим оба случая.
Статья основана на официальной документации Kotlin, ссылки в конце статьи.
Реализация интерфейса через делегирование
Шаблон делегирования является хорошей альтернативой наследованию, и Kotlin поддерживает его нативно, освобождая вас от необходимости написания шаблонного кода.
В примере ниже класс Derived
реализует интерфейс Base
через делегирование всех public членов интерфейса объекту b
, передаваемому в конструктор:
interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b fun main(args: Array<String>) { val b = BaseImpl(10) Derived(b).print() // prints 10 }
Ключевое слово by
в объявлении Derived
говорит о том, что объект b
типа Base
будет храниться внутри экземпляра Derived
и компилятор сгенерирует у Derived
соответствующие методы из Base
, которые при вызове будут переданы объекту b
.
Переопределение члена интерфейса, реализованного через делегирование
Переопределение через override работает как обычно: компилятор будет использовать override
реализацию вместо реализации по умолчанию из объекта-делегата. Так, после добавления override fun printMessage() { print("abc") }
в класс Derived
, программа напечатает "abc" вместо "10" при вызове printMessage
:
interface Base { fun printMessage() fun printMessageLine() } class BaseImpl(val x: Int) : Base { override fun printMessage() { print(x) } override fun printMessageLine() { println(x) } } class Derived(b: Base) : Base by b { override fun printMessage() { print("abc") } } fun main() { val b = BaseImpl(10) Derived(b).printMessage() Derived(b).printMessageLine() }
Заметьте, что члены интерфейса, переопределенные таким способом, не будут доступны из членов объекта-делегата:
interface Base { val message: String fun print() } class BaseImpl(val x: Int) : Base { override val message = "BaseImpl: x = $x" override fun print() { println(message) } } class Derived(b: Base) : Base by b { // Это свойство недоступно из метода 'print' объекта-делегата 'b' override val message = "Message of Derived" } fun main() { val b = BaseImpl(10) val derived = Derived(b) derived.print() println(derived.message) }
Делегированные свойства
Существует несколько основных видов свойств, которые мы реализовываем каждый раз вручную в случае их надобности. Однако намного удобнее было бы реализовать их раз и навсегда и положить в какую-нибудь библиотеку. Примеры таких свойств:
- ленивые (lazy) свойства: значение вычисляется один раз при первом обращении,
- свойства, на события об изменении которых можно подписаться (observable properties),
- свойства, хранимые в ассоциативном списке, а не в отдельных полях.
Для таких случаев, Kotlin поддерживает делегированные свойства:
class Example { var p: String by Delegate() }
Их синтаксис выглядит следующим образом: val/var <имя свойства>: <Тип> by <выражение>
. Выражение после by – делегат: обращения (get()
, set()
) к свойству будут обрабатываться этим выражением. Делегат не обязан реализовывать какой-то интерфейс – достаточно, чтобы у него были методы getValue()
и setValue()
с определённой сигнатурой:
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()
принимает два аргумента: начальное значение свойства и обработчик (лямбда), который вызывается при изменении свойства. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение.
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 }
Локальные делегированные свойства
Примечание: доступно в Kotlin, начиная с версии 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()
,value
— новое значение, должно быть того же типа, что и свойство (или его родительского типа).
Функции getValue()
и/или setValue()
могут быть предоставлены либо как члены класса-делегата, либо как его расширения. Последнее полезно, когда вам нужно делегировать свойство объекту, который изначально не имеет этих функций. Обе эти функции должны быть отмечены с помощью ключевого слова operator
.
Класс-делегат может реализовывать один из интерфейсов ReadOnlyProperty
или ReadWriteProperty
, содержащих необходимые 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) }
Правила трансляции
Для каждого делегированного свойства компилятор 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()
со сгенерированным кодом из правил трансляции (когда 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
влияет только на создание вспомогательного свойства и не влияет на код, генерируемый геттером или сеттером.
Источники:
- Делегирование (рус, eng)
- Делегированные свойства (рус, eng)