Scope functions в Kotlin — это пять функций стандартной библиотеки (let, run, with, apply, also), которые выполняют блок кода в контексте объекта. Они различаются способом доступа к объекту (через it или this) и возвращаемым значением (контекстный объект или результат лямбды). По данным документации Kotlin (2026), правильный выбор scope function сокращает шаблонный код на 25–40%. Scope functions помогают писать инициализацию, конфигурацию и преобразования объектов в декларативном стиле.
Главное
Scope functions (функции области видимости) — это набор из пяти функций (let, run, with, apply, also), встроенных в стандартную библиотеку Kotlin. Каждая из них принимает лямбду и временно изменяет область видимости так, что внутри лямбды код выполняется в контексте конкретного объекта. Это избавляет от повторения имени объекта при множественных вызовах и группирует связанные операции в один блок.
Две ключевые характеристики различают scope functions: способ доступа к контекстному объекту (через this или через it) и возвращаемое значение (сам контекстный объект или результат лямбды). Доступ через this делает объект неявным получателем — методы и свойства вызываются без префикса. Доступ через it передаёт объект как явный параметр лямбды. Возврат объекта позволяет строить цепочки, возврат результата — присваивать преобразованное значение.
Исторически scope functions появились как замена классическому паттерну Builder и временным переменным для инициализации. Вместо создания отдельного builder-класса разработчик пишет apply { свойство1 = ...; свойство2 = ... }. Это снижает количество кода и делает инициализацию более читаемой. В экосистеме Kotlin scope functions используются во всех фреймворках и библиотеках как стандартная идиома языка.
Выбор правильной scope function определяется двумя вопросами: хотите ли вы использовать this или it для доступа к объекту, и хотите ли вы вернуть сам объект или результат лямбды. Комбинация двух бинарных признаков даёт четыре возможных варианта, а with занимает особое место как единственная функция, не являющаяся расширением.
| Функция | Доступ | Возврат | Расширение? | Типичный сценарий |
|---|---|---|---|---|
| let | it | результат лямбды | да | Преобразование, null-проверка |
| run | this | результат лямбды | да | Вычисление с контекстом |
| with | this | результат лямбды | нет | Группировка вызовов |
| apply | this | контекстный объект | да | Инициализация свойств |
| also | it | контекстный объект | да | Побочные действия, логирование |
Для конфигурации объекта без возврата нового значения используйте apply (доступ через this) или also (доступ через it). Для вычисления нового значения на основе объекта используйте let (доступ через it) или run (доступ через this). Для группировки вызовов без цепочки используйте with. Следование этой схеме делает код предсказуемым для других разработчиков, знакомых с Kotlin.
apply — scope function, которая возвращает контекстный объект и предоставляет доступ к нему через this. Это идеальный инструмент для инициализации свойств объекта после создания. Внутри блока apply можно устанавливать свойства без повторения имени объекта. Метод возвращает сам объект, что позволяет встраивать apply в цепочки или использовать в инициализаторах.
data class ServerConfig(
var host: String = "localhost",
var port: Int = 8080,
var useTls: Boolean = false
)
val config = ServerConfig().apply {
host = "api.example.com"
port = 443
useTls = true
}
В листинге apply конфигурирует ServerConfig, обращаясь к свойствам напрямую через this (this опущен). Без apply пришлось бы писать config.host = ..., config.port = ... — три повторяющихся ссылки на одну и ту же переменную. Возврат объекта позволяет сохранить результат конфигурации в val config. also, в отличие от apply, передаёт объект через it и подходит для побочных действий.
fun saveUser(user: User) {
validate(user).also { result ->
println("Validation result: $result")
}
val savedUser = user.also {
log("Saving user ${it.id}")
database.save(it)
}
}
В примере also используется для логирования — возвращая исходный объект, он выполняет побочное действие (вывод сообщения) без изменения объекта. Это цепочка: validate(user) возвращает User, also логирует результат и передаёт пользователя дальше. also также полезен для добавления элементов в коллекции или отладки в середине цепочки преобразований.
let — scope function, возвращающая результат лямбды и передающая контекст через it. Это основная функция для null-безопасных преобразований: если объект nullable, комбинация ?.let { } выполнит блок только при ненулевом значении. let также используется для ограничения области видимости временных переменных и map-подобных преобразований одного типа в другой.
fun findUser(id: Int): User?
val displayName = findUser(42)?.let { user ->
"${user.name} (${user.email})"
} ?: "Unknown user"
// let with collection transformation
val numbers = listOf("1", "2", "3")
val parsed = numbers.firstOrNull()?.let {
it.toIntOrNull()
} ?: 0
В первом примере let вызывается через ?. — блок выполняется, только если findUser вернул не null. Лямбда принимает user через it (можно переименовать для ясности) и возвращает форматированную строку. Если пользователь не найден — Elvis-оператор ?: даёт значение по умолчанию. Во втором примере let преобразует строку в число с потенциальным null.
run — аналог let с доступом через this, возвращающий результат лямбды. run удобен для вычислений, где требуется контекст объекта, но возвращается новое значение. Вызов run() без объекта (как run { ... }) создаёт временную область видимости для переменных. with — не-расширение с доступом через this, подходящее для группировки вызовов без цепочки.
// run as context-driven computation
val transformed = listOf(1, 2, 3).run {
filter { it > 1 }.map { it * 10 }
}
println(transformed) // [20, 30]
// with for grouping calls
val info = with(StringBuilder()) {
append("Name: ")
append("Alice")
toString()
}
В примере run на списке возвращает результат фильтрации и маппинга — само преобразование, а не список. with принимает StringBuilder как аргумент (не через точку) и внутри работает с this: append, append, toString. Результат with — строка info. Выбор между run и with часто субъективен, но with предпочтительнее, когда объект передаётся из другого места без цепочки вызовов.
with отличается от остальных scope functions тем, что не является функцией-расширением. Она принимает объект первым аргументом, а лямбду — вторым. Внутри лямбды объект доступен через this. with удобен для группировки вызовов методов объекта без сохранения результата или когда требуется временно «войти» в контекст для чтения нескольких свойств.
val user = User("Alice", "alice@example.com", "Admin")
val summary = with(user) {
"User $name has role $role and email $email"
}
println(summary)
// with for UI component initialization
with(TextView(context)) {
text = "Hello"
textSize = 18.0f
setTextColor(Color.BLUE)
}
В первом примере with формирует строку summary, обращаясь к свойствам user напрямую через this. Без with пришлось бы писать "User ${user.name} has role ${user.role}" — многократное повторение user. Во втором примере with группирует конфигурацию TextView: все методы и свойства вызываются на одном объекте без повторения. Недостаток with — невозможность использовать в цепочке, так как он не является расширением и не возвращает контекстный объект.
Часто задаваемые вопросы
Используйте apply — он возвращает объект и предоставляет доступ через this, позволяя устанавливать свойства без повторения имени объекта. also подходит, если нужен явный параметр it для отличаемости от свойств объекта.
let передаёт контекст через it (явный параметр), run — через this (неявный получатель). let удобен для null-проверок и преобразований, run — для вычислений с контекстом. Обе возвращают результат лямбды.
with не является расширением — он принимает объект аргументом. Используйте with для группировки вызовов, когда не нужна цепочка. apply — расширение, возвращающее объект, удобное в инициализаторах и builder-паттернах.
Да, но будьте осторожны с this — вложенные this создают путаницу. Используйте let или also с явными именами для вложенных контекстов, чтобы избежать неоднозначности при обращении к внешнему объекту.
Все scope functions — inline, поэтому они не создают накладных расходов на анонимные классы. Разница в производительности между ними пренебрежимо мала. Выбор должен основываться на читаемости и конвенциях, а не на скорости.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также