Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
---
|
||||
title: Делегирование
|
||||
description: Делегирование интерфейсов через by, делегаты свойств и границы автоматического перенаправления
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Делегирование - передача работы другому объекту. Kotlin поддерживает его на уровне языка словом `by`, поэтому композиция требует не больше кода, чем наследование.
|
||||
|
||||
## Делегирование интерфейса
|
||||
```kotlin
|
||||
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`. Унаследованной реализации при этом нет - есть ссылка на другой объект.
|
||||
|
||||
## Переопределение части методов
|
||||
Делегировать можно всё, кроме того, что нужно изменить:
|
||||
```kotlin
|
||||
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`-класса и был бы связан с его внутренностями.
|
||||
|
||||
## Делегирование нескольких интерфейсов
|
||||
```kotlin
|
||||
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("текст")) // записано: текст
|
||||
}
|
||||
```
|
||||
Это даёт эффект множественного наследования реализации - но без его проблем, потому что каждый интерфейс обслуживает отдельный объект.
|
||||
|
||||
## Расширение коллекции
|
||||
Классическая задача, которую нельзя безопасно решить наследованием:
|
||||
```kotlin
|
||||
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`.
|
||||
|
||||
### Готовые делегаты
|
||||
```kotlin
|
||||
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
|
||||
}
|
||||
```
|
||||
Подробнее: [Свойства](Свойства.md)
|
||||
|
||||
### Делегирование свойства в Map
|
||||
```kotlin
|
||||
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`:
|
||||
```kotlin
|
||||
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`:
|
||||
```kotlin
|
||||
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
|
||||
Позволяет проверить свойство при создании делегата - например, что имя есть в конфигурации:
|
||||
```kotlin
|
||||
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 x` |
|
||||
> | `Supplier` + `volatile` для ленивости | `by lazy` |
|
||||
> | `PropertyChangeSupport` | `Delegates.observable` |
|
||||
> | нет аналога делегатов свойств | `by` с `getValue`/`setValue` |
|
||||
>
|
||||
> **Делегирование интерфейса - главная экономия.** В Java декоратор над интерфейсом из десяти методов требует десять методов-перенаправлений, даже если менять нужно один. Любой новый метод в интерфейсе нужно добавить во все декораторы. Kotlin генерирует перенаправление сам:
|
||||
> ```java
|
||||
> 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](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Абстрактные классы](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Делегат не знает о переопределениях - это главная ловушка.** Если один метод делегата вызывает другой, он вызовет **свою** версию, а не вашу:
|
||||
> ```kotlin
|
||||
> 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` остаются реализациями по умолчанию, то есть по ссылке:
|
||||
> ```kotlin
|
||||
> 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](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||||
>
|
||||
> **Делегат фиксируется при создании** и не может быть заменён:
|
||||
> ```kotlin
|
||||
> 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`.
|
||||
>
|
||||
> **Делегат свойства - объект на каждый экземпляр:**
|
||||
> ```kotlin
|
||||
> class Heavy {
|
||||
> val a: String by lazy { "a" }
|
||||
> val b: String by lazy { "b" } // ещё один объект Lazy
|
||||
> }
|
||||
>
|
||||
> fun main() { println(Heavy().a + Heavy().b) } // ab
|
||||
> ```
|
||||
> Для класса с десятками ленивых свойств, создаваемого миллионами экземпляров, это заметный расход памяти.
|
||||
>
|
||||
> **Делегирование в `Map` падает в рантайме при отсутствии ключа:**
|
||||
> ```kotlin
|
||||
> 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()` и другие пути.
|
||||
|
||||
Дополнительно: [Свойства](Свойства.md)
|
||||
Далее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md)
|
||||
Reference in New Issue
Block a user