Ключевое слово by встречается в Kotlin в двух ролях: делегирование реализации интерфейса и делегированные свойства. Первое заменяет наследование композицией, второе даёт lazy, observable, by viewModels() и by remember.
Делегирование класса
interface Repository { fun load(id: Int): String; fun save(v: String) }
class LoggingRepository(private val inner: Repository) : Repository by inner {
override fun save(v: String) {
println("save $v")
inner.save(v)
}
}
Компилятор сам сгенерирует все методы интерфейса, переадресовав их inner, а переопределить можно только нужные. Это паттерн «декоратор» без шаблонного кода и способ следовать правилу «композиция лучше наследования».
Тонкость: делегат фиксируется в момент создания объекта. Если inner объявлен как var и потом поменялся, вызовы всё равно уйдут старому объекту. И если делегат внутри себя вызывает свой же метод, переопределение в обёртке он не увидит: this у делегата это он сам, а не обёртка.
Делегированные свойства
class Profile {
val avatar: Bitmap by lazy { loadAvatar() }
var name: String by Delegates.observable("") { _, old, new -> println("$old -> $new") }
}
Синтаксис val x: T by d означает: геттер свойства вызывает d.getValue(this, property), а сеттер d.setValue(...). Делегатом может быть любой объект с такими операторами.
lazy
by lazy { } вычисляет значение при первом обращении и запоминает его. Режимы:
LazyThreadSafetyMode.SYNCHRONIZED(по умолчанию): блокировка, инициализатор выполнится ровно один раз даже при гонке потоков;PUBLICATION: инициализатор может выполниться несколько раз параллельно, но сохранится первое значение;NONE: без синхронизации, быстрее, только для однопоточного кода (например, главный поток Android).
Если инициализатор бросил исключение, значение не запоминается, и следующее обращение попробует снова.
lazy работает только с val. Частая ошибка в Android: val binding by lazy { ... view ... } во фрагменте, где view пересоздаётся. Ленивое значение запомнит view первого создания и будет держать его (утечка и работа со старой view).
observable и vetoable
var count: Int by Delegates.observable(0) { prop, old, new -> render() }
var age: Int by Delegates.vetoable(0) { _, _, new -> new >= 0 } // отклонит отрицательные
var token: String by Delegates.notNull() // как lateinit, но работает и с примитивами
Делегирование в Map
class Config(map: Map<String, Any?>) {
val host: String by map
val port: Int by map
}
Config(mapOf("host" to "localhost", "port" to 8080)).port // 8080
Удобно для JSON-подобных структур, но без проверки типов при компиляции.
Свой делегат
class Trimmed : ReadWriteProperty<Any?, String> {
private var value = ""
override fun getValue(thisRef: Any?, property: KProperty<*>) = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim()
}
}
class Form { var email by Trimmed() }
Можно и без интерфейса, достаточно функций operator fun getValue и setValue. Если делегату нужно знать имя свойства при создании, реализуют provideDelegate.
Делегаты в Android
val vm: MainViewModel by viewModels()иby activityViewModels(): ленивое получение ViewModel из нужногоViewModelStoreOwner.val args by navArgs<DetailsArgs>().- В Compose
var text by remember { mutableStateOf("") }:StateреализуетgetValue/setValueкак расширения, поэтому работаетby. by preferencesи подобные самописные делегаты дляSharedPreferences/DataStore.
Частые ошибки
by lazyна значении, зависящем от жизненного цикла (view, context фрагмента).- Ожидать, что смена
var-делегата при делегировании класса что-то изменит. - Использовать
LazyThreadSafetyMode.NONEв многопоточном коде. - Путать
Delegates.notNull()иlateinit: первый работает с примитивами, но без проверкиisInitialized.
Что спросят на собесе
- Как работает
by lazyи какие у него режимы потокобезопасности? - Что такое делегирование класса и чем оно лучше наследования?
- Как написать свой делегат свойства?
- Как работает
by viewModels()? - Почему
byработает сmutableStateOfв Compose?