Зачем нужен let, also, apply, run, with — и в чём разница между ними?

В Kotlin функции let, also, apply, run, with — это scope functions (функции области видимости), которые помогают писать более компактный, читаемый и выразительный код. Все они позволяют выполнить блок кода с объектом, не обязательно повторяя имя объекта. Однако каждая из них отличается контекстом (this или it) и значением, которое возвращается из лямбды.

let

Используется, когда нужно выполнить операцию с объектом (например, проверить его на null или трансформировать), не изменяя его.

  • Контекст: it

  • Возвращает: результат последнего выражения в лямбде

val name: String? = "Kotlin"
val length = name?.let {
println("Длина строки: ${it.length}")
it.length
}

Часто используется в сочетании с ?. для безопасного вызова:

user?.let { println("User name: ${it.name}") }

also

Применяется, когда нужно выполнить побочные действия (например, логгирование или проверку), не изменяя сам объект.

  • Контекст: it
  • Возвращает: сам объект (как this), на котором был вызван also
val list = mutableListOf("A", "B", "C")
.also { println("Начальный список: $it") }

Используется, когда важно сохранить цепочку вызовов, но внутри блока выполнить доп. действия:

val person = Person("Alex").also { println("Создан: ${it.name}") }

apply

Применяется, когда нужно инициализировать объект. Позволяет обращаться к членам объекта напрямую через this.

  • Контекст: this

  • Возвращает: сам объект

val paint = Paint().apply {
color = Color.RED
strokeWidth = 10f
}

Часто используется при конфигурации или создании объектов:

val textView = TextView(context).apply {
text = "Hello"
textSize = 20f
setTextColor(Color.BLACK)
}

run

Сочетает поведение let и apply: выполняет действия над объектом и возвращает результат вычислений.

  • Контекст: this
  • Возвращает: результат лямбды
val length = "Kotlin".run {
println("Обрабатываем строку: $this")
length
}

Также может вызываться как обычная функция:

val result = run {
val a = 5
val b = 10
a + b
}

Используется, когда нужно работать с объектом и вернуть результат, а также удобно при локальном scope.

with

Функция, а не метод объекта. Применяется для группировки операций над объектом, когда нет необходимости возвращать сам объект.

  • Контекст: this
  • Возвращает: результат лямбды
val result = with(StringBuilder()) {
append("Hello, ")
append("World!")
toString()
}

Синтаксически похоже на run, но вызов происходит через with(obj) { ... }, а не obj.run { ... }.

Таблица сравнения

Функция Контекст Возвращает Назначение
let it Результат лямбды Трансформация, работа с nullable
--- --- --- ---
also it Сам объект Побочные действия (лог, проверка)
--- --- --- ---
apply this Сам объект Инициализация и настройка объекта
--- --- --- ---
run this Результат лямбды Выполнение операций и получение результата
--- --- --- ---
with this Результат лямбды Работа с объектом без расширения его API
--- --- --- ---

Примеры для сравнения

// let
val email = "test@example.com"
email.let {
println("Email длина: ${it.length}")
}
// also
val list = mutableListOf("A", "B").also {
println("Список перед добавлением: $it")
it.add("C")
}
// apply
val config = AppConfig().apply {
host = "localhost"
port = 8080
}
// run
val messageLength = "Hello".run {
println("Внутри run: $this")
length
}
// with
val result = with(StringBuilder()) {
append("Start")
append(" + Middle")
append(" + End")
toString()
}

Каждая из этих функций помогает делать код чище и выразительнее, если использовать их осознанно в зависимости от задачи: нужно ли получить объект, результат выражения или просто выполнить побочные действия.