Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+372
View File
@@ -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)