Avatar ·

Когда использовать run, let, apply, also и with в Kotlin? Примеры и объяснение

Всем привет! Я только начал изучать Kotlin после Java, и меня сильно запутали функции области видимости (scope functions): run, let, apply, also и with. Я прочитал документацию, но всё равно не понимаю, когда какую из них применять. Например, я часто вижу такой код: ```kotlin val numbers = mutableListOf("one", "two", "three") numbers.let { println(it) } numbers.apply { add("four") } numbers.run { println("Size: $size") } ``` Но почему нельзя просто написать `println(numbers)` или `numbers.add("four")`? В чём разница между `let` и `apply`? И зачем нужен `also`? Ещё я пытался инициализировать объект с конфигурацией: ```kotlin val person = Person().apply { name = "John" age = 30 } ``` Это работает, но то же самое можно сделать и с `with`: ```kotlin val person = with(Person()) { name = "John" age = 30 this } ``` В чём разница? И когда использовать `run` вместо `apply`? Буду благодарен за чёткие правила и примеры для каждой функции. Хочется понять не только синтаксис, но и логику выбора. Заранее спасибо! <<>>

Краткая шпаргалка: как выбирать функцию области видимости

Все пять функций (let, apply, also, run, with) выполняют блок кода в контексте объекта. Главное различие — как они возвращают результат и как обращаются к объекту внутри блока. Вот ключевые правила:

  • let — возвращает результат лямбды, объект доступен как it. Используйте для цепочек вызовов и проверок на null.
  • apply — возвращает сам объект, внутри блока this. Используйте для конфигурации объекта (Builder-паттерн).
  • also — возвращает сам объект, внутри блока it. Используйте для побочных действий (логирование, валидация).
  • run — возвращает результат лямбды, внутри блока this. Используйте для вычислений на основе объекта.
  • with — возвращает результат лямбды, внутри блока this. Используйте для группировки операций над объектом без возврата самого объекта.

Подробный разбор с примерами

1. let — для non-null проверок и цепочек

val name: String? = "Alice"
// Без let пришлось бы писать if (name != null) ...
name?.let { 
    println("Имя: $it") // it — это name (не null)
    it.length         // возвращаем длину
} ?: println("Имя null")

Когда использовать: когда нужно выполнить код только если объект не null, или когда нужно преобразовать объект в цепочке вызовов (например, data.map { ... }.firstOrNull()?.let { ... }).

2. apply — для конфигурации объекта

data class Person(var name: String = "", var age: Int = 0)

val person = Person().apply {
    name = "John"   // this.name = "John"
    age = 30        // this.age = 30
}
// person возвращается автоматически

Когда использовать: для инициализации свойств объекта без необходимости писать person.name = ... каждый раз. Идеально подходит для Java-стиля Builder или настройки View в Android.

3. also — для побочных эффектов

val numbers = mutableListOf(1, 2, 3)
numbers.also {
    println("Текущий список: $it") // логируем
}.add(4) // продолжаем цепочку

Когда использовать: когда нужно выполнить действие, не влияющее на основной поток (логирование, проверка, запись в кэш). В отличие от apply, вы не конфигурируете объект, а просто "замечаете" его.

4. run — для вычислений с контекстом this

val result = "Hello".run {
    // this — это "Hello"
    println("Длина: $length") // this.length
    length > 3               // возвращаем Boolean
}
println(result) // true

Когда использовать: когда нужно выполнить несколько операций над объектом и вернуть какой-то результат (не сам объект). run также удобен для проверки null: nullableObject?.run { ... }.

5. with — для группировки операций (без возврата объекта)

val sb = StringBuilder()
val result = with(sb) {
    append("Hello, ")
    append("world!")
    toString() // возвращаем результат
}
println(result) // Hello, world!

Когда использовать: когда нужно выполнить несколько действий над объектом, но сам объект вам не нужен — только результат. Обратите внимание: with — не extension-функция, а обычная функция высшего порядка.

Сравнительная таблица

Функция | Доступ к объекту | Возвращаемое значение
--------|------------------|---------------------
let     | it               | Результат лямбды
apply   | this             | Сам объект
also    | it               | Сам объект
run     | this             | Результат лямбды
with    | this             | Результат лямбды

Практический совет

Начните с двух правил:

  • Если нужно сконфигурировать объект и вернуть его — apply.
  • Если нужно проверить на null или преобразовать — let.
  • Для всего остального используйте run или with.

Со временем вы привыкнете к этим функциям и начнёте использовать их интуитивно.

<<>>

Альтернативный взгляд: когда НЕ использовать scope functions

Хотя функции области видимости — мощный инструмент, новички часто злоупотребляют ими. Вот когда от них лучше отказаться:

1. Если код становится менее читаемым

// Плохо: вложенные scope functions
user?.let { u ->
    u.address?.let { a ->
        a.city?.let { c ->
            println(c)
        }
    }
}

// Лучше: использовать safe call + let один раз
user?.address?.city?.let { println(it) }

2. Если вы используете apply только ради синтаксического сахара

// Избыточно
val list = mutableListOf(1, 2, 3).apply {
    add(4)
}

// Проще
val list = mutableListOf(1, 2, 3)
list.add(4)

3. С with — осторожно в Android

В Android-разработке with(view) { ... } может привести к конфликту имён, если у вас есть свойства с теми же именами, что и у View. Используйте apply или явные вызовы.

Мой личный чек-лист

  • let — только для non-null проверок и цепочек (не для простых вызовов).
  • apply — только для инициализации объектов (не для вызова методов).
  • also — только для логирования/отладки (не для изменения состояния).
  • run — для вычислений, когда нужен результат.
  • with — для группировки операций, но лучше заменить на run, если можно.

Помните: Kotlin — это не соревнование, кто использует больше scope functions. Код должен быть понятным в первую очередь.