Делегированные свойства
Существуют определённые распространённые типы свойств, которые, хотя мы можем реализовать их вручную каждый раз, когда нам это нужно, было бы очень удобно реализовать один раз и навсегда и поместить в библиотеку. Примеры включают:
- ленивые свойства: значение вычисляется только при первом обращении;
- наблюдаемые свойства: слушатели получают уведомления о изменениях этого свойства;
- хранение свойств в карте вместо отдельного поля для каждого свойства.
Для обработки этих (и других) случаев Kotlin поддерживает делегированные свойства:
class Example {
var p: String by Delegate()
}
Синтаксис: val/var <property name>: <Type> by <expression>. Выражение после 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, который делегирует экземпляру Delegate, вызывается функция 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 1.1, вы можете объявлять делегированное свойство внутри функции или блока кода, оно не обязательно должно быть членом класса. Ниже вы можете найти пример.
Стандартные делегаты
Стандартная библиотека Kotlin предоставляет фабричные методы для нескольких полезных видов делегатов.
Ленивые свойства
lazy() — это функция, которая принимает лямбда-выражение и возвращает экземпляр Lazy<T>, который может служить делегатом для реализации ленивого свойства: первое обращение к get() выполняет лямбда-выражение, переданное в lazy(), и запоминает результат; последующие обращения к get() просто возвращают запомненный результат.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одной потоке, и все потоки увидят одно и то же значение. Если синхронизация делегата инициализации не требуется, чтобы несколько потоков могли выполнить её одновременно, передайте 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() {
val user = User()
user.name = "first"
user.name = "second"
}
Если вы хотите перехватывать присвоения и "ветовать" их, используйте vetoable() вместо observable(). Обработчик, переданный в vetoable, вызывается перед выполнением присвоения нового значения свойства.
Делегирование другому свойству
Начиная с Kotlin 1.4, свойство может делегировать свой getter и setter другому свойству. Такая делегация доступна как для свойств верхнего уровня, так и для свойств класса (членов и расширений). Делегируемое свойство может быть:
- свойством верхнего уровня
- членским или расширяющим свойством того же класса
- членским или расширяющим свойством другого класса
Для делегирования свойства другому свойству используйте соответствующий квалификатор :: в имени делегата, например, this::delegate или MyClass::delegate.
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
//sampleStart
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
//sampleEnd
Это может быть полезно, например, когда вы хотите переименовать свойство обратной совместимостью: вы вводите новое свойство, аннотируете старое свойство аннотацией @Deprecated и делегируете его реализацию.
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// Notification: 'oldName: Int' is deprecated.
// Use 'newName' instead
myClass.oldName = 42
println(myClass.newName) // 42
}
Хранение свойств в карте
Распространённым случаем использования является хранение значений свойств в карте. Это часто встречается в приложениях, таких как парсинг 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(): 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 as val
var readWrite: Int by resourceDelegate()
Правила трансляции
Внутри, для каждого делегированного свойства Kotlin-компилятор генерирует вспомогательное свойство и делегирует ему. Например, для свойства prop генерируется скрытое свойство prop$delegate, и код аксессоров просто делегирует этому дополнительному свойству:
class C {
var prop: Type by MyDelegate()
}
// this code is generated by the compiler instead:
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 — это объект рефлексии типа KProperty, описывающий prop само.
Обратите внимание, что синтаксис this::prop для ссылки на привязанную ссылку на вызываемый объект в коде напрямую доступен только начиная с Kotlin 1.1.
Обеспечение делегата
Определяя оператор 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)
// create delegate
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 и выполняет необходимую валидацию сразу же.
Без этой возможности перехватить привязку между свойством и его делегатом, для достижения той же функциональности пришлось бы явно передавать имя свойства, что неудобно:
// Checking the property name without "provideDelegate" functionality
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)
// create delegate
}
В сгенерированном коде вызывается метод provideDelegate для инициализации вспомогательного свойства prop$delegate. Сравните сгенерированный код для объявления свойства val prop: Type by MyDelegate() с сгенерированным кодом выше (когда метод provideDelegate отсутствует):
class C {
var prop: Type by MyDelegate()
}
// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
// calling "provideDelegate" to create the additional "delegate" property
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
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/delegated-properties.html