17 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Делегирование | Делегирование интерфейсов через by, делегаты свойств и границы автоматического перенаправления |
|
Делегирование - передача работы другому объекту. Kotlin поддерживает его на уровне языка словом by, поэтому композиция требует не больше кода, чем наследование.
Делегирование интерфейса
interface Logger5 {
fun log(msg: String): String
fun warn(msg: String): String
}
class ConsoleLogger3 : Logger5 {
override fun log(msg: String) = "[LOG] $msg"
override fun warn(msg: String) = "[WARN] $msg"
}
class Service15(logger: Logger5) : Logger5 by logger {
fun work() = log("начинаю работу")
}
fun main() {
val s = Service15(ConsoleLogger3())
println(s.work()) // [LOG] начинаю работу
println(s.warn("внимание")) // [WARN] внимание — метод перенаправлен
}
Service15 реализует Logger5, но код методов не пишет: компилятор генерирует перенаправление на logger. Унаследованной реализации при этом нет - есть ссылка на другой объект.
Переопределение части методов
Делегировать можно всё, кроме того, что нужно изменить:
interface Repository3 {
fun find(id: Int): String?
fun save(id: Int, value: String): String
}
class DbRepository : Repository3 {
private val data = mutableMapOf<Int, String>()
override fun find(id: Int) = data[id]
override fun save(id: Int, value: String): String {
data[id] = value
return "сохранено"
}
}
class CachingRepository(private val origin: Repository3) : Repository3 by origin {
private val cache = mutableMapOf<Int, String>()
override fun find(id: Int): String? = // только этот метод свой
cache.getOrPut(id) { origin.find(id) ?: return null }
}
fun main() {
val repo = CachingRepository(DbRepository())
println(repo.save(1, "Иван")) // сохранено — делегировано
println(repo.find(1)) // Иван — через кеш
println(repo.find(1)) // Иван — уже из кеша
println(repo.find(99)) // null
}
Это декоратор в три строки. На наследовании он потребовал бы open-класса и был бы связан с его внутренностями.
Делегирование нескольких интерфейсов
interface Reader { fun read(): String }
interface Writer2 { fun write(data: String): String }
class FileReader2 : Reader { override fun read() = "данные из файла" }
class FileWriter2 : Writer2 { override fun write(data: String) = "записано: $data" }
class ReadWriteFile(
reader: Reader,
writer: Writer2,
) : Reader by reader, Writer2 by writer
fun main() {
val f = ReadWriteFile(FileReader2(), FileWriter2())
println(f.read()) // данные из файла
println(f.write("текст")) // записано: текст
}
Это даёт эффект множественного наследования реализации - но без его проблем, потому что каждый интерфейс обслуживает отдельный объект.
Расширение коллекции
Классическая задача, которую нельзя безопасно решить наследованием:
class CountingList3<T>(private val inner: MutableList<T> = mutableListOf())
: MutableList<T> by inner {
var addedCount = 0
private set
override fun add(element: T): Boolean {
addedCount++
return inner.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
addedCount += elements.size
return inner.addAll(elements)
}
}
fun main() {
val list = CountingList3<Int>()
list.add(1)
list.addAll(listOf(2, 3))
println("${list.size} элементов, добавлений: ${list.addedCount}") // 3 элементов, добавлений: 3
println(list.toList()) // [1, 2, 3]
}
При наследовании от ArrayList счётчик мог бы удвоиться, если addAll внутри вызывает add. С делегированием внутренние вызовы inner остаются внутри inner и наших переопределений не касаются.
Обратите внимание на list.toList(): сам println(list) вывел бы CountingList3@5fa7e7ff, потому что toString - член Any, а члены Any делегирование не перенаправляет.
Эта же особенность - источник главной ловушки делегирования, см. подводные камни.
Делегаты свойств
Тот же by для свойства передаёт работу объекту с методами getValue и setValue.
Готовые делегаты
import kotlin.properties.Delegates
class Form3 {
val config: String by lazy { "вычислено один раз" }
var name: String by Delegates.observable("") { _, old, new ->
println("name: '$old' -> '$new'")
}
var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 }
var port: Int by Delegates.notNull()
}
fun main() {
val f = Form3()
println(f.config) // вычислено один раз
f.name = "Иван" // name: '' -> 'Иван'
f.age = 200 // отклонено
println(f.age) // 0
f.port = 8080
println(f.port) // 8080
}
Подробнее: Свойства
Делегирование свойства в Map
class Settings(private val map: Map<String, Any?>) {
val host: String by map
val port: Int by map
}
fun main() {
val s = Settings(mapOf("host" to "localhost", "port" to 8080))
println("${s.host}:${s.port}") // localhost:8080
}
Имя свойства становится ключом. Для var нужен MutableMap.
Свой делегат
Нужны операторы getValue и (для var) setValue:
import kotlin.reflect.KProperty
class NonBlank(private var value: String = "") {
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
require(newValue.isNotBlank()) { "${property.name} не может быть пустым" }
value = newValue.trim()
}
}
class User6 {
var name: String by NonBlank()
}
fun main() {
val u = User6()
u.name = " Иван "
println("'${u.name}'") // 'Иван'
try {
u.name = " "
} catch (e: IllegalArgumentException) {
println(e.message) // name не может быть пустым
}
}
property даёт имя и метаданные свойства - удобно для сообщений об ошибках и для чтения из конфигурации по имени.
Для делегата только на чтение достаточно реализовать ReadOnlyProperty:
import kotlin.properties.ReadOnlyProperty
fun envVar(name: String) = ReadOnlyProperty<Any?, String> { _, _ ->
System.getenv(name) ?: "<не задано>"
}
class Config7 {
val home: String by envVar("HOME")
val missing: String by envVar("НЕТ_ТАКОЙ_ПЕРЕМЕННОЙ")
}
fun main() {
println(Config7().home.isNotEmpty()) // true
println(Config7().missing) // <не задано>
}
provideDelegate
Позволяет проверить свойство при создании делегата - например, что имя есть в конфигурации:
import kotlin.reflect.KProperty
class Required(private val source: Map<String, String>) {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): Required {
require(property.name in source) { "нет ключа '${property.name}'" }
return this
}
operator fun getValue(thisRef: Any?, property: KProperty<*>) = source[property.name]!!
}
fun main() {
val source = mapOf("host" to "localhost")
class Ok3 { val host: String by Required(source) }
println(Ok3().host) // localhost
try {
class Bad4 { val port: String by Required(source) }
Bad4()
} catch (e: IllegalArgumentException) {
println(e.message) // нет ключа 'port'
}
}
Проверка происходит при создании объекта, а не при первом чтении - ошибка обнаруживается раньше.
[!info] Отличия от Java
Java Kotlin все методы перенаправлять вручную byгенерирует ихдекоратор = класс с полем и 10 методами class D(x: I) : I by xSupplier+volatileдля ленивостиby lazyPropertyChangeSupportDelegates.observableнет аналога делегатов свойств byсgetValue/setValueДелегирование интерфейса - главная экономия. В Java декоратор над интерфейсом из десяти методов требует десять методов-перенаправлений, даже если менять нужно один. Любой новый метод в интерфейсе нужно добавить во все декораторы. Kotlin генерирует перенаправление сам:
class CachingRepository implements Repository { private final Repository origin; CachingRepository(Repository origin) { this.origin = origin; } public String save(int id, String v) { return origin.save(id, v); } // шум public String find(int id) { /* своя логика */ } }Делегатов свойств в Java нет совсем. Ленивое поле требует ручной двойной проверки блокировки, наблюдаемое -
PropertyChangeSupportи явных вызовов. В Kotlin этоby lazyиby Delegates.observable.Паттерн Декоратор перестаёт быть паттерном - он становится однострочной конструкцией языка, поэтому его применяют там, где в Java предпочли бы наследование «чтобы не писать много кода».
Сравнить: Полиморфизм подтипов в Java, Абстрактные классы
[!warning] Подводные камни Делегат не знает о переопределениях - это главная ловушка. Если один метод делегата вызывает другой, он вызовет свою версию, а не вашу:
interface Sender { fun send(msg: String): String fun sendAll(msgs: List<String>): String } class BasicSender : Sender { override fun send(msg: String) = "отправлено: $msg" override fun sendAll(msgs: List<String>) = msgs.joinToString { send(it) } } class LoggingSender(private val inner: Sender) : Sender by inner { var logged = 0 private set override fun send(msg: String): String { logged++ return inner.send(msg) } } fun main() { val s = LoggingSender(BasicSender()) s.sendAll(listOf("a", "b")) println("залогировано: ${s.logged}") // 0 — наш send не вызвался! }
sendAllделегирован и внутри вызываетsendобъектаinner. Чтобы счётчик работал, нужно переопределить иsendAll. Это обратная сторона изоляции: наследование здесь сработало бы «как ожидается», но стало бы хрупким.Члены
Anyне делегируются.toString,equalsиhashCodeостаются реализациями по умолчанию, то есть по ссылке:class Wrapped<T>(private val inner: MutableList<T> = mutableListOf()) : MutableList<T> by inner fun main() { val a = Wrapped<Int>().apply { add(1) } val b = Wrapped<Int>().apply { add(1) } println(a.toString()) // Wrapped@4554617c, а не [1] println(a == b) // false, хотя содержимое одинаково println(a == listOf(1)) // false }Обёртка над коллекцией, попавшая в
Setили в сравнение, поведёт себя не как коллекция. Эти три метода нужно переопределять вручную. Подробнее: equals, hashCode и toStringДелегат фиксируется при создании и не может быть заменён:
fun interface Source { fun get(): String } class Service16(private var source: Source) : Source by source { fun replace(newSource: Source) { source = newSource } // не влияет на by! } fun main() { val s = Service16 { "первый" } s.replace { "второй" } println(s.get()) // первый — by хранит исходную ссылку }
byкопирует ссылку в скрытое поле при создании объекта. Для подменяемого делегата нужно перенаправлять вручную.Делегирование нельзя сделать
by thisили сослаться на собственное свойство в заголовке - объект ещё не создан.Из делегированных методов нет доступа к
thisобёртки. Делегат не знает, что его кто-то оборачивает, - он не может вызвать методы обёртки.
by lazyнаvarневозможен - толькоval.Делегат свойства - объект на каждый экземпляр:
class Heavy { val a: String by lazy { "a" } val b: String by lazy { "b" } // ещё один объект Lazy } fun main() { println(Heavy().a + Heavy().b) } // abДля класса с десятками ленивых свойств, создаваемого миллионами экземпляров, это заметный расход памяти.
Делегирование в
Mapпадает в рантайме при отсутствии ключа:class Settings2(map: Map<String, Any?>) { val host: String by map } fun main() { val s = Settings2(emptyMap()) try { println(s.host) } catch (e: NoSuchElementException) { println("ключ отсутствует") // ключ отсутствует } }Проверить это заранее помогает
provideDelegate.Делегирование
MutableListраскрывает полный изменяемый API. Обёртка, делегирующаяMutableList, позволяет внешнему коду добавлять элементы в обход вашей логики - черезiterator().remove(),subList()и другие пути.
Дополнительно: Свойства Далее: Вложенные и внутренние классы