KotlinMiddleTechnical
Как работает делегирование (delegation) в Kotlin с ключевым словом by?
Ключевое слово by реализует два механизма: делегирование интерфейса (компилятор генерирует forwarding-методы) и делегирование свойства (компилятор вызывает getValue/setValue делегата). Оба варианта избавляют от шаблонного кода.
Два вида делегирования через by
В Kotlin by используется в двух контекстах:
- Class delegation —
class A(b: B) : InterfaceX by b. Компилятор генерирует forwarding-методы, перекидывающие вызовы на объектb. - Property delegation —
val x by SomeDelegate(). Компилятор превращает обращение к свойству в вызовыgetValue(thisRef, property)и, еслиvar,setValue(thisRef, property, value).
Class delegation: паттерн Decorator без шаблонного кода
interface Cache {
fun get(key: String): String?
fun put(key: String, value: String)
fun clear()
}
class InMemoryCache : Cache {
private val map = mutableMapOf<String, String>()
override fun get(key: String) = map[key]
override fun put(key: String, value: String) { map[key] = value }
override fun clear() = map.clear()
}
// LoggingCache перекрывает только put, остальное делегирует
class LoggingCache(private val delegate: Cache) : Cache by delegate {
override fun put(key: String, value: String) {
println("PUT $key")
delegate.put(key, value)
}
}
fun main() {
val cache: Cache = LoggingCache(InMemoryCache())
cache.put("user:1", "Alice") // PUT user:1
println(cache.get("user:1")) // Alice
}
Property delegation: встроенные делегаты
Стандартная библиотека предоставляет готовые делегаты:
lazy { }— ленивая инициализация, потокобезопасная по умолчанию (LazyThreadSafetyMode.SYNCHRONIZED).Delegates.observable(initial) { prop, old, new -> ... }— callback при изменении.Delegates.vetoable(initial) { prop, old, new -> new > 0 }— условное присвоение.by map— хранение свойств вMap<String, Any?>.
class Config(private val props: Map<String, Any?>) {
val host: String by props
val port: Int by props
}
fun main() {
val cfg = Config(mapOf("host" to "localhost", "port" to 5432))
println("${cfg.host}:${cfg.port}") // localhost:5432
}
Собственный делегат свойства
import kotlin.reflect.KProperty
class EnvDelegate(private val envKey: String, private val default: String = "") {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
System.getenv(envKey) ?: default
}
fun envVar(key: String, default: String = "") = EnvDelegate(key, default)
object AppConfig {
val dbUrl: String by envVar("DATABASE_URL", "jdbc:postgresql://localhost/dev")
val redisHost: String by envVar("REDIS_HOST", "localhost")
}
Подводные камни
- Переопределение метода при class delegation. Если не переопределить метод явно, делегат вызывается напрямую — это может нарушить инварианты обёртки (например, счётчик вызовов не увеличится).
- Скрытый lifecycle делегата. Если делегат хранит корутин или подписку, владеющий класс не управляет его отменой — нужно явно передавать CoroutineScope или реализовывать Closeable.
- lazy не всегда потокобезопасен.
lazy(LazyThreadSafetyMode.NONE)быстрее, но небезопасен в многопоточной среде. - by map требует совпадения имени. Ключ в Map должен точно совпадать с именем свойства (case-sensitive); опечатка приведёт к NoSuchElementException в рантайме.
- Delegates.observable не валидирует. Он вызывается уже после присвоения — для валидации используйте vetoable.
- Делегирование к изменяемому объекту. Если delegate реализует интерфейс, но хранит мутируемое состояние, два экземпляра делегирующего класса разделят это состояние — нарушение изоляции.
- Отражение и serialization. Kotlinx Serialization не поддерживает property delegates из коробки; нужны кастомные сериализаторы.
- Перегрузка операторов и by. Компилятор ищет
operator fun getValue— если метод не помеченoperator, получите ошибку компиляции.
Common mistakes
- Объяснять «delegation и ключевое слово by» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Проблемы начинаются, когда delegate скрывает mutable state или lifecycle, который владелец класса не контролирует.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «delegation и ключевое слово by» своими словами и связывает ее с кодом.
- Называет механизм: Для интерфейса компилятор генерирует forwarding methods, для свойства вызывает getValue/setValue у delegate.
- Видит production-последствие: Проблемы начинаются, когда delegate скрывает mutable state или lifecycle, который владелец класса не контролирует.