Avatar ·

Что делает ключевое слово `by` в Kotlin? Объясните на примерах

Всем привет! Я начал изучать Kotlin недавно, перешел с Java, и столкнулся с непонятным для меня синтаксисом. Встречаю в коде коллег ключевое слово `by`, но не могу понять его назначение. Например, я видел такой код: ```kotlin class MyClass { var name: String by Delegates.observable("Initial") { prop, old, new -> println("$old -> $new") } } ``` Или вот такое: ```kotlin class Derived(base: Base) : Base by base ``` Я понимаю, что это как-то связано с делегированием, но мне не ясно, как это работает под капотом. В Java у нас были просто наследование и интерфейсы, а тут какая-то магия. Объясните, пожалуйста, для чего используется `by`? В каких случаях его применять? И как это работает на уровне байт-кода? Буду благодарен за простые примеры, чтобы разобраться новичку. <<>>

Ключевое слово `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() // Инициализация обязательна до первого чтения

Главное преимущество делегатов — инкапсуляция логики работы со свойствами. Вы пишете её один раз, а затем переиспользуете в любых классах.