Что делает ключевое слово `by` в Kotlin? Объясните на примерах
Ключевое слово `by` в Kotlin: два основных назначения
Ключевое слово by в Kotlin реализует паттерн делегирования (delegation). У него есть два принципиально разных применения: делегирование свойств и делегирование классов. Разберем оба.
1. Делегирование свойств (Property Delegation)
Синтаксис val/var имя: Тип by делегат означает, что геттеры и сеттеры свойства будут "перепоручены" другому объекту-делегату. Это избавляет от написания шаблонного кода.
Стандартные делегаты в Kotlin:
lazy { ... }— ленивая инициализация (вычисляется один раз при первом обращении)Delegates.observable(...)— отслеживание измененийDelegates.vetoable(...)— изменение с возможностью отменыby map— хранение свойств в Map
Пример с lazy:
class Database {
val config: Config by lazy {
println("Загрузка конфигурации...")
loadConfig() // тяжелая операция
}
fun loadConfig(): Config = Config(...)
}
Пример с observable:
class User {
var name: String by Delegates.observable("") { _, old, new ->
println("Имя изменено: '$old' -> '$new'")
}
}
fun main() {
val user = User()
user.name = "Иван" // Вывод: Имя изменено: '' -> 'Иван'
user.name = "Петр" // Вывод: Имя изменено: 'Иван' -> 'Петр'
}
Пример с Map:
class Person(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val person = Person(mapOf(
"name" to "Анна",
"age" to 25
))
println("${person.name}, ${person.age} лет") // Анна, 25 лет
}
2. Делегирование классов (Class Delegation)
Синтаксис class Derived(base: Base) : Base by base реализует композицию вместо наследования. Класс Derived автоматически получает все методы интерфейса Base, но их реализация делегируется объекту base.
Пример:
interface SoundMaker {
fun makeSound(): String
}
class Dog : SoundMaker {
override fun makeSound() = "Гав!"
}
class Cat : SoundMaker {
override fun makeSound() = "Мяу!"
}
// Класс-делегат
class TalkingPet(pet: SoundMaker) : SoundMaker by pet
fun main() {
val dog = TalkingPet(Dog())
val cat = TalkingPet(Cat())
println(dog.makeSound()) // Гав!
println(cat.makeSound()) // Мяу!
}
Как это работает под капотом?
Компилятор Kotlin генерирует для класса TalkingPet все методы интерфейса SoundMaker, которые просто вызывают соответствующие методы у переданного объекта pet. Это избавляет от ручного написания кода вида:
class TalkingPet(pet: SoundMaker) : SoundMaker {
override fun makeSound() = pet.makeSound() // пришлось бы писать вручную
}
Когда использовать?
- Делегирование свойств — когда нужно добавить поведение к свойству (ленивость, отслеживание, валидацию) без изменения класса
- Делегирование классов — когда нужно переиспользовать реализацию, но наследование не подходит (например, класс уже extends другой класс)
Ключевое слово by — это мощный инструмент для уменьшения шаблонного кода и улучшения читаемости. Начните с использования встроенных делегатов (lazy, observable), а затем пробуйте создавать свои собственные.
Создание собственных делегатов
Помимо встроенных делегатов, вы можете создавать свои. Для этого нужно реализовать интерфейсы ReadOnlyProperty или ReadWriteProperty.
Пример: делегат для валидации email:
class EmailDelegate : ReadWriteProperty<Any?, String> {
private var value = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
if (!value.contains("@")) {
throw IllegalArgumentException("Некорректный email: $value")
}
this.value = value
}
}
class User {
var email: String by EmailDelegate()
}
fun main() {
val user = User()
user.email = "test@example.com" // OK
// user.email = "test" // Ошибка!
}
Дополнительный совет
Для быстрого создания делегата можно использовать Delegates.notNull() — он работает как lateinit, но для типов, которые не поддерживают lateinit (например, примитивы):
var count: Int by Delegates.notNull() // Инициализация обязательна до первого чтения
Главное преимущество делегатов — инкапсуляция логики работы со свойствами. Вы пишете её один раз, а затем переиспользуете в любых классах.