Avatar ·

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

Привет, коллеги! Я начал изучать Kotlin и никак не могу разобраться с функциями-расширениями run, let, apply, also и with. В документации написано, что они помогают писать более чистый код, но я постоянно путаюсь, какую из них выбрать. Например, у меня есть простой класс User: ```kotlin data class User(var name: String, var age: Int, var email: String) ``` И я хочу создать экземпляр, изменить его поля, а потом вывести в лог. Я пробовал разные варианты: ```kotlin // Вариант 1 val user = User("", 0, "") user.name = "Иван" user.age = 25 user.email = "ivan@mail.ru" println(user) // Вариант 2 val user = User("", 0, "").apply { name = "Иван" age = 25 email = "ivan@mail.ru" } println(user) ``` Вроде бы apply работает, но когда нужно вернуть результат преобразования, я теряюсь. Например, если я хочу получить длину имени после изменения: ```kotlin val length = User("", 0, "").let { it.name = "Иван" it.name.length } ``` Или вот ещё пример: я хочу проверить, что объект не null, и выполнить действие. Знаю, что let часто используют для этого, но и with вроде бы похож. Подскажите, пожалуйста, как различать эти функции и в каких случаях каждую из них применять? Желательно с простыми примерами, чтобы было понятно новичку. Заранее спасибо! <<>>

Основное правило: смотрим на контекст и возвращаемое значение

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

Краткая шпаргалка

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

Пример 1: Настройка объекта — apply

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

val user = User().apply {
    name = "Иван"
    age = 25
    email = "ivan@mail.ru"
}
// user уже заполнен, можно использовать

Пример 2: Побочное действие — also

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

val user = User("Иван", 25, "ivan@mail.ru")
    .also { println("Создан пользователь: ${it.name}") }
    .also { if (it.age < 18) throw Exception("Малолетний") }

Пример 3: Преобразование с null-check — let

Самый частый use-case — работа с nullable объектами. let выполняется только если объект не null.

val userName: String? = getUserName()
userName?.let { 
    println("Имя пользователя: $it, длина: ${it.length}")
}

Также let хорош для цепочек преобразований:

val emailLength = User("Иван", 25, "ivan@mail.ru")
    .let { it.email }
    .let { it.length }  // вернёт Int = 12

Пример 4: Вычисления с контекстом объекта — run

Когда нужно выполнить несколько операций с объектом и вернуть какой-то результат. Объект доступен как this.

val isAdult = user.run {
    age >= 18 && name.isNotBlank()  // вернёт Boolean
}

Пример 5: Группировка вызовов — with

Не является функцией-расширением, но удобна, когда у вас уже есть объект и нужно много раз обратиться к его методам.

with(user) {
    println("Имя: $name, возраст: $age")
    sendEmail(email)
}

Сводная таблица для быстрого выбора

ФункцияКонтекстВозвращаетКогда использовать
applythisобъектНастройка объекта
alsoitобъектПобочные эффекты
letitрезультатПреобразования, null-check
runthisрезультатВычисления с объектом
withthisрезультатГруппировка вызовов

Совет: начните с apply и let — они покрывают 80% случаев. Остальные добавляйте постепенно, когда почувствуете необходимость.

<<>>

Дополнение: цепочки вызовов и читаемость

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

Пример: создаём пользователя, проверяем возраст, логируем и отправляем приветствие.

val result = User()
    .apply { 
        name = "Анна"
        age = 30
    }
    .also { println("Создан: ${it.name}") }
    .let { user ->
        if (user.age >= 18) "Взрослый: ${user.name}" 
        else "Ребёнок: ${user.name}"
    }
    .also { println("Результат проверки: $it") }
// result — строка "Взрослый: Анна"

Обратите внимание: apply настраивает объект, also логирует, let преобразует, и снова also для побочного действия. Каждая функция чётко говорит о своём назначении. Если бы мы написали всё через run, код был бы менее очевидным.

Ещё один лайфхак: если вы видите, что внутри блока let или run вы не используете результат, а просто вызываете методы — скорее всего, нужно apply или also. И наоборот, если внутри apply вы что-то вычисляете и пытаетесь вернуть — переходите на run.

Главное — не бойтесь экспериментировать. Со временем вы научитесь выбирать функцию интуитивно, просто потому что она лучше смотрится в конкретном контексте.