type | layout | category | title | url |
---|---|---|---|---|
doc |
reference |
Syntax |
Делегированные свойства |
За помощь в переводе спасибо официальному блогу JetBrains на Хабрахабре
Существует несколько основных видов свойств, которые мы реализовываем каждый раз вручную в случае их надобности. Однако намного удобнее было бы реализовать их раз и навсегда и положить в какую-нибудь библиотеку. Примеры таких свойств:
- ленивые свойства (lazy properties): значение вычисляется один раз, при первом обращении
- свойства, на события об изменении которых можно подписаться (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()
это функция, которая принимает лямбду и возвращает экземпляр класса 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
, который не гарантирует никакой потокобезопасности.
Функция 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
}
Вы можете объявить локальные переменные как делегированные свойства. Например, вы можете сделать локальную переменную ленивой:
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)
}
Для каждого делегированного свойства компилятор 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
влияет только на создание вспомогательного свойства и не влияет на код, генерируемый геттером или сеттером.