Kotlin: run, let, apply, also, with — когда что использовать? Полный разбор с примерами
Привет, друг! Если ты изучаешь Kotlin и хочешь писать чистый, лаконичный код, то рано или поздно столкнёшься с пятью волшебными функциями: run, let, apply, also и with. Это scope functions (функции области видимости) — они позволяют выполнять код в контексте объекта и здорово упрощают жизнь.
Но вот незадача: многие новички (и не только) путаются, когда что применять. Этот вопрос — один из самых популярных на Stack Overflow, и сегодня мы раз и навсегда разберёмся, как не ошибиться. Мы не просто перечислим правила — мы покажем реальные примеры из практики, которые ты сможешь сразу использовать в своих проектах.
Что такое scope functions и зачем они нужны?
Scope functions — это функции, которые создают временную область видимости для объекта. Внутри этой области ты можешь обращаться к объекту через it или this. Главное отличие между ними — как они возвращают результат и как ссылаются на контекст.
Вот краткая шпаргалка, которую мы будем наполнять примерами:
let— контекст:it, возвращает: результат лямбды. Используется для ненулевых проверок и цепочек вызовов.run— контекст:this, возвращает: результат лямбды. Универсальная функция, часто для инициализации и вычислений.apply— контекст:this, возвращает: сам объект. Идеален для настройки свойств объекта.also— контекст:it, возвращает: сам объект. Хорош для дополнительных действий (логирование, проверки).with— не extension-функция, контекст:this, возвращает: результат лямбды. Удобен для группировки вызовов одного объекта без создания нового.
Запомни главное правило: выбор функции зависит от того, что тебе нужно — вернуть объект или результат, и как ты хочешь обращаться к контексту (через it или this).
1. let — король безопасных цепочек
let — это твой лучший друг, когда нужно работать с nullable объектами. Он выполняет код только если объект не равен null. Контекст передаётся как it.
Пример: безопасное преобразование
val name: String? = "Андрей"
// Без let:
if (name != null) {
println("Длина имени: ${name.length}")
}
// С let (гораздо элегантнее):
name?.let {
println("Длина имени: ${it.length}")
}
let также отлично подходит для цепочек операций. Например, когда нужно преобразовать один объект в другой:
val numbers = listOf(1, 2, null, 4, null, 6)
val nonNullSquared = numbers
.mapNotNull { it }
.let { list ->
list.map { it * it }
}
println(nonNullSquared) // [1, 4, 16, 36]
Когда использовать let:
- Проверка на null (самый частый кейс).
- Преобразование одного значения в другое.
- Выполнение цепочки операций с результатом.
2. apply — конструктор без лишних слов
apply возвращает сам объект, а внутри лямбды контекст — this. Это идеальный инструмент для настройки объекта перед использованием. Забудь про длинные блоки set!
Пример: настройка View (Android) или data class
data class User(var name: String = "", var age: Int = 0)
val user = User().apply {
name = "Мария"
age = 25
}
println(user) // User(name=Мария, age=25)
// Ещё круче — настройка TextView:
val textView = TextView(context).apply {
text = "Привет, Kotlin!"
textSize = 18f
setTextColor(Color.BLUE)
}
Обрати внимание: мы не пишем this.name — Kotlin сам подставляет контекст. apply возвращает настроенный объект, что позволяет сразу присвоить его переменной.
Когда использовать apply:
- Инициализация свойств объекта (особенно UI-компонентов).
- Построение объекта с множеством параметров.
- Работа с Builder-паттерном (например, AlertDialog).
3. also — шпион, который не мешает
also похож на apply, но контекст — it. Он тоже возвращает объект, но используется для побочных эффектов: логирование, отладка, валидация — всё то, что не меняет сам объект.
Пример: логирование при цепочке вызовов
val numbers = mutableListOf(1, 2, 3)
numbers
.also { println("Исходный список: $it") }
.add(4)
.also { println("После добавления: $numbers") }
.let { numbers.filter { it > 2 } }
.also { println("Отфильтрованные: $it") }
Видишь магию? also позволяет вставить логирование, не разрывая цепочку. Это гораздо удобнее, чем писать отдельные println.
Когда использовать also:
- Логирование и отладка.
- Дополнительные проверки (например, перед сохранением).
- Любые действия, которые не должны влиять на основной поток.
4. run — универсальный солдат
run — это гибрид: он и контекст this, и возвращает результат лямбды. Очень удобен, когда нужно выполнить несколько операций с объектом и вернуть какой-то итог.
Пример: вычисление на основе объекта
val user = User("Иван", 30)
val description = user.run {
"Пользователь: $name, возраст: $age"
}
println(description) // Пользователь: Иван, возраст: 30
// А ещё run отлично работает с nullable:
val nullableUser: User? = null
val greeting = nullableUser?.run {
"Привет, $name!"
} ?: "Пользователь не найден"
println(greeting) // Пользователь не найден
Когда использовать run:
- Когда нужно выполнить несколько операций и вернуть результат.
- Безопасная работа с nullable (в паре с
?.). - Альтернатива
with, когда объект nullable