Делегированные свойства
За помощь в переводе спасибо официальному блогу 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