Делегирование в Kotlin
Шаблон делегированияШаблон делегирования — это фундаментальная техника в шаблонах проектирования программного обеспечения. В шаблоне делегирования два объекта участвуют в обработке одного и того же запроса. Объект, который получает запрос, делегирует запрос другому объекту для обработки.
Kotlin напрямую поддерживает шаблон делегирования, делая его более элегантным и лаконичным. Kotlin реализует делегирование через ключевое слово `by`.
Делегирование классов
Делегирование классов означает, что методы, определённые в классе, фактически реализуются путём вызова методов объекта другого класса.
В следующем примере производный класс `Derived` наследует все методы интерфейса `Base` и делегирует входящему объекту класса `Base` выполнение этих методов.
// 创建接口
interface Base {
fun print()
}
// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 输出 10
}
В объявлении `Derived` конструкция `by` означает, что `b` хранится внутри экземпляра объекта `Derived`, а компилятор сгенерирует все методы, унаследованные от интерфейса `Base`, и перенаправит вызовы к `b`.
Делегирование свойств
Делегирование свойств означает, что значение свойства класса задаётся не напрямую в классе, а поручается классу-делегату, что позволяет обеспечить единообразное управление свойствами класса.
Синтаксис делегирования свойств:
val/var <属性名>: <类型> by <表达式>
- var/val: тип свойства (изменяемое/только для чтения)
- Имя свойства: имя свойства
- Тип: тип данных свойства
- Выражение: класс делегата
Выражение после ключевого слова `by` является делегатом. Метод `get()` свойства (и метод `set()`) будет делегирован методам `getValue()` и `setValue()` этого объекта. Делегирование свойства не требует реализации какого-либо интерфейса, но должно предоставлять функцию `getValue()` (для свойств `var` также требуется функция `setValue()`).
Определение класса-делегата
Этот класс должен содержать методы `getValue()` и `setValue()`. Параметр `thisRef` — это объект класса, выполняющего делегирование, а `prop` — объект делегируемого свойства.
import kotlin.reflect.KProperty
// 定义包含属性委托的类
class Example {
var p: String by Delegate()
}
// 委托的类
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 属性赋值为 $value")
}
}
fun main(args: Array<String>) {
val e = Example()
println(e.p) // 访问该属性,调用 getValue() 函数
e.p = "Example" // 调用 setValue() 函数
println(e.p)
}
Результат вывода:
Example@433c675d, 这里委托了 p 属性 Example@433c675d 的 p 属性赋值为 Example Example@433c675d, 这里委托了 p 属性
Стандартные делегаты
Стандартная библиотека Kotlin содержит множество встроенных фабричных методов для реализации делегирования свойств.
Ленивые свойства
`lazy()` — это функция, которая принимает лямбда-выражение в качестве параметра и возвращает экземпляр `Lazy<T>`. Возвращённый экземпляр можно использовать как делегат для реализации ленивых свойств: первый вызов `get()` выполняет лямбда-выражение, переданное в `lazy()`, и записывает результат; последующие вызовы `get()` просто возвращают записанный результат.
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}
Вывод после выполнения:
computed! Hello Hello
Наблюдаемые свойства
`observable` можно использовать для реализации шаблона наблюдателя.
Функция `Delegates.observable()` принимает два параметра: первый — начальное значение, а второй — обработчик события изменения значения свойства.
После присваивания свойства выполняется обработчик события. У него три параметра: свойство, которому присваивается значение, старое значение и новое значение:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("初始值") {
prop, old, new ->
println("旧值:$old -> 新值:$new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "第一次赋值"
user.name = "第二次赋值"
}
Вывод после выполнения:
Old value: initial value -> New value: first assignment Old value: first assignment -> New value: second assignment
Хранение свойств в Map
Распространённый сценарий использования — хранение значений свойств в map. Это часто встречается в приложениях, которые парсят JSON или занимаются другими «динамическими» вещами. В этом случае можно использовать сам экземпляр map в качестве делегата для реализации делегированных свойств.
class Site(val map: Map<String, Any?>) {
val name: String by map
val url: String by map
}
fun main(args: Array<String>) {
// 构造函数接受一个映射参数
val site = Site(mapOf(
"name" to "Example",
"url" to "www.example.com"
))
// 读取映射值
println(site.name)
println(site.url)
}
Вывод после выполнения:
Example www.example.com
Если вы используете свойство `var`, нужно заменить `Map` на `MutableMap`:
class Site(val map: MutableMap<String, Any?>) {
val name: String by map
val url: String by map
}
fun main(args: Array<String>) {
var map:MutableMap<String, Any?> = mutableMapOf(
"name" to "Example",
"url" to "www.example.com"
)
val site = Site(map)
println(site.name)
println(site.url)
println("--------------")
map.put("name", "Google")
map.put("url", "www.google.com")
println(site.name)
println(site.url)
}
Вывод после выполнения:
Example www.example.com -------------- Google www.google.com
Не null
`notNull` подходит для ситуаций, когда значение свойства нельзя определить на этапе инициализации.
class Foo {
var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)
Следует отметить, что если обратиться к свойству до его присваивания, будет выброшено исключение.
Локальные делегированные свойства
Можно объявлять локальные переменные как делегированные свойства. Например, можно сделать локальную переменную лениво инициализируемой:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
Переменная `memoizedFoo` вычисляется только при первом доступе. Если `someCondition` не выполняется, переменная вообще не будет вычислена.
Требования к делегированию свойств
Для свойств только для чтения (т.е. свойств `val`) их делегат должен предоставлять функцию с именем `getValue()`. Эта функция принимает следующие параметры:
- `thisRef` — должен иметь тот же тип, что и тип владельца свойства (для свойств-расширений — расширяемый тип), или его супертип.
- `property` — должен иметь тип `KProperty<*>` или его супертип.
Эта функция должна возвращать тот же тип, что и свойство (или его подтип).
Для изменяемого свойства (т.е. свойства `var`), в дополнение к функции `getValue()`, его делегат также должен предоставлять функцию с именем `setValue()`. Эта функция принимает следующие параметры:
- `property` — должен иметь тип `KProperty<*>` или его супертип.
- `new value` — должен иметь тот же тип, что и свойство, или его супертип.
Правила преобразования
За каждой реализацией делегированного свойства компилятор 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` — это объект рефлексии типа `KProperty`, который описывает сам `prop`.
Предоставление делегата
Определив оператор `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<*>` или его супертип.
Во время создания экземпляра `MyUI` метод `provideDelegate` вызывается для каждого свойства, и необходимая проверка выполняется немедленно.
Без этой возможности перехватывать привязку между свойством и его делегатом для достижения той же функциональности вам пришлось бы явно передавать имя свойства, что не очень удобно:
// 检查属性名称而不使用“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` влияет только на создание вспомогательного свойства и не влияет на код, сгенерированный для геттера или сеттера.
Другие расширения