Несколько переменных в let в Kotlin: 3 элегантных способа

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Несколько переменных в let: проблема, которую вы не замечали

Вы когда-нибудь писали на Kotlin и вдруг понимали: "Мне нужно обработать сразу две nullable переменные"? Вы тянетесь к любимому let, но... стоп. let работает только с одной переменной. А как быть, если у вас их две, три или больше?

Этот вопрос — один из самых популярных на Stack Overflow по теме Kotlin. Тысячи разработчиков спотыкаются об эту, казалось бы, простую задачу. И сегодня мы раз и навсегда разберемся, как красиво и правильно решить её.

В этой статье мы не просто дадим сухие примеры. Мы сравним 3 подхода, обсудим их плюсы, минусы и best practices. Вы узнаете, какой метод выбрать в зависимости от ситуации, и перестанете писать уродливые вложенные let.

Почему проблема "Multiple variable let" так популярна?

Kotlin — язык, который обожает null safety. Функции области видимости (scope functions) — let, run, with, apply, also — это наше секретное оружие для безопасной работы с nullable типами. Но у них есть ограничение: каждая функция рассчитана на один объект-получатель.

Представьте ситуацию: вы получаете из API два nullable поля — имя пользователя и его email. Вам нужно проверить, что оба не null, и только потом выполнить действие (например, отправить приветственное письмо).

Новички часто пишут так:

val name: String? = getUserName()
val email: String? = getUserEmail()

if (name != null && email != null) {
    sendWelcomeEmail(name, email)
}

Это работает, но это не Kotlin-way. Мы можем сделать код более лаконичным и безопасным. Давайте посмотрим, как.

Подход 1: Вложенные let — просто, но некрасиво

Самый очевидный способ — вложить один let в другой. Это первое, что приходит в голову:

val name: String? = getUserName()
val email: String? = getUserEmail()

name?.let { safeName ->
    email?.let { safeEmail ->
        sendWelcomeEmail(safeName, safeEmail)
    }
}

Плюсы:

  • Работает "из коробки", не требует дополнительных функций
  • Код читается последовательно

Минусы:

  • При увеличении количества переменных (3, 4 и более) код превращается в "пирамиду ужаса"
  • Сложно читать и поддерживать
  • Каждый вложенный уровень добавляет лишний отступ

Этот подход подходит только для двух переменных. Для трех и более — ищите альтернативу.

Подход 2: Использование Pair, Triple и data class

Kotlin предоставляет встроенные классы Pair и Triple. Идея в том, чтобы объединить nullable переменные в один объект, а затем проверить его через let.

val name: String? = getUserName()
val email: String? = getUserEmail()

val pair = name?.let { n ->
    email?.let { e -> n to e }
}

pair?.let { (safeName, safeEmail) ->
    sendWelcomeEmail(safeName, safeEmail)
}

Для трех переменных используем Triple:

val name: String? = getUserName()
val email: String? = getUserEmail()
val age: Int? = getUserAge()

val triple = name?.let { n ->
    email?.let { e ->
        age?.let { a -> Triple(n, e, a) }
    }
}

triple?.let { (safeName, safeEmail, safeAge) ->
    sendWelcomeEmail(safeName, safeEmail, safeAge)
}

Плюсы:

  • Нет глубокой вложенности в финальном вызове
  • Деструктуризация делает код чистым

Минусы:

  • Внутренняя вложенность (при создании Pair/Triple) все еще есть
  • Triple — это максимум для стандартной библиотеки. Для 4+ переменных нужно создавать свой data class
  • Создание временных объектов может быть незначительно затратно, но в 99% случаев это не проблема

Это хороший баланс между простотой и читаемостью. Для 2-3 переменных — отличный выбор.

Подход 3: Кастомная функция — максимальная гибкость

Самый элегантный и масштабируемый способ — написать свою функцию-расширение. Это решение особенно популярно в коммерческих проектах. Давайте создадим универсальную функцию для двух nullable переменных:

inline fun <T1, T2, R> let2(
    value1: T1?,
    value2: T2?,
    block: (T1, T2) -> R
): R? {
    return if (value1 != null && value2 != null) {
        block(value1, value2)
    } else {
        null
    }
}

// Использование
val name: String? = getUserName()
val email: String? = getUserEmail()

let2(name, email) { safeName, safeEmail ->
    sendWelcomeEmail(safeName, safeEmail)
}

А вот версия для трех переменных:

inline fun <T1, T2, T3, R> let3(
    value1: T1?,
    value2: T2?,
    value3: T3?,
    block: (T1, T2, T3) -> R
): R? {
    return if (value1 != null && value2 != null && value3 != null) {
        block(value1, value2, value3)
    } else {
        null
    }
}

Плюсы:

  • Максимальная читаемость и лаконичность
  • Нет вложенности
  • Легко масштабировать на любое количество переменных
  • Можно добавить логирование или другие действия перед вызовом block

Минусы:

  • Нужно написать и поддерживать свой код (но это делается один раз)
  • Для разных проектов может понадобиться разное количество аргументов

Этот подход — выбор профессионалов. Он делает код чистым и понятным, особенно когда nullable переменных много.

Сравнительная таблица подходов

Блоги

Рекомендаций по книгам

Подход Читаемость Масштабируемость Сложность Лучший случай
Вложенные let Низкая (пирамида) Плохая (3+ переменных) Низкая Только 2 переменные, быстрый фикс