В языке Kotlin стандартные функции высшего порядка apply, also, let и run появились с целью упрощения chainable-конфигурирования объектов и локальных изменений с минимальным количеством бойлерплейта. Эти функции облегчают работу с изменяемыми и неизменяемыми объектами, позволяют лаконично выражать цепочки преобразований, а также снимают часть проблем с временным областями видимости переменных.
Эти функции были взяты из паттерна builder и подходов fluent-интерфейсов. Их появление обязано стремлению сделать код чище и избавить его от излишнего объявления вспомогательных переменных.
Обычный подход требует многократного обращения к объекту или выноса временных переменных. Это снижает читабельность и увеличивает риск ошибок:
val user = User() user.name = "Alex" user.age = 26 user.isActive = true
Использование функций apply, also, let, run повышает выразительность:
val user = User().apply { name = "Alex" age = 26 isActive = true }
Краткие описания:
data class User(var name: String = "", var age: Int = 0, var isActive: Boolean = false) val configuredUser = User().apply { name = "Alice" age = 30 isActive = true } debugUser(configuredUser.also { println("User is configured: $it") }) val emailLength = configuredUser.email?.let { it.length } ?: 0 val description = configuredUser.run { "$name ($age)" }
Ключевые особенности:
Может ли функция let изменить объект, с которым работает?
Функция let не предназначена для изменения объекта. Она скорее служит для применения преобразования к значению и возвращает результат. Следует помнить, что внутри let вместо this доступен it.
val upperName = user.name.let { it.uppercase() } // let не меняет user
В чем отличие между apply и run?
Обе работают с контекстом this внутри лямбды, разница в возвращаемом значении:
// apply val building = StringBuilder().apply { append("start-") append("end") } // building — это StringBuilder // run val result = StringBuilder().run { append("start-") append("end") toString() } // result — это String
Можно ли вложить применение apply и let? Если да, то когда это оправдано?
Да, вложение мягко рекомендуется для агрегации или пошаговой настройки объектов, особенно при работе с nullable:
val userInfo = user?.apply { isActive = true }?.let { "${it.name} is active: ${it.isActive}" }
Везде в коде используются let для модификации объекта, смешиваются apply и let — в итоге объект не меняется там, где ожидалось, а его значение теряется в цепочках.
Плюсы:
Минусы:
Используются apply и also для настройки и логирования, let только для работы с nullable и получения результата, run — для цепочек преобразований, требующих вычисления нового значения.
Плюсы:
Минусы: