JavaRush /Курсы /Kotlin SELF /takeIf и takeUnless: охранные условия и связка с Elvis ?:...

takeIf и takeUnless: охранные условия и связка с Elvis ?:

Kotlin SELF
27 уровень , 4 лекция
Открыта

1. Введение

Когда вы только начинаете писать код, if кажется универсальной отвёрткой: им можно и проверить, и присвоить, и распечатать, и даже «воспитать» пользователя сообщением “введите нормально”. И это правда. Но со временем (иногда уже через два файла кода) возникает новая проблема: вам часто нужно не «ветвиться», а «отфильтровать значение».

Представьте типичную ситуацию из нашего консольного приложения (мы продолжаем развивать CLI‑приложение с командами вроде add, list, remove и т.п.). Пользователь вводит строку, а мы хотим:

  • взять её, если она нормальная (не пустая, не из пробелов, не -1, не "abc" вместо числа),
  • иначе получить null, чтобы дальше красиво обработать это через уже знакомую null‑safety и Elvis ?:.

Вот тут takeIf и takeUnless — как маленькие «фильтры» для значения.

2. Что такое takeIf: «оставь значение, если оно проходит проверку»

takeIf — это extension‑функция, которая вызывается на самом значении и возвращает:

  • это же значение, если условие истинно,
  • null, если условие ложно.

То есть логика читается примерно так: «возьми это, если…».

Сигнатуру можно запомнить не как страшную математику, а как смысл:

value.takeIf { условиеПроValue }  // вернёт value или null

Ключевой эффект: результат становится nullable (T?), а значит, мы можем дальше применить:

  • safe call ?.
  • Elvis ?:
  • обычную проверку if (x != null)

В Kotlin‑документации и примерах вы часто встретите стиль .takeIf { it >= 0 } ?: ... как компактный способ «взять значение, только если оно валидно».

Мини‑таблица для мозга

Инструмент Идея Возвращает
takeIf { cond }
«Оставь значение, если cond = true»
T?
takeUnless { cond }
«Оставь значение, если cond = false»
T?
?:
«Если слева null — возьми справа» не‑null результат (обычно)

3. Elvis ?: в этой связке: «если не подошло — подставь fallback»

С Elvis‑оператором вы знакомились в теме про null‑safety: выражение вида

val x = nullableValue ?: defaultValue

означает: если слева null, берём defaultValue.

Почему takeIf почти всегда дружит с ?:? Потому что takeIf специально и делает вам null как «маркер невалидности», а ?: превращает этот маркер в понятное поведение: значение по умолчанию, сообщение об ошибке, ранний выход из функции, или что-то ещё.

4. Пример: валидируем результат indexOf

Сейчас будет ситуация из реальной жизни строк: indexOf(...) возвращает -1, если не нашёл. Это не исключение и не ошибка JVM, это просто «так договорились». Но -1 нельзя использовать как индекс для substring.

Обычно пишут так:

val at = email.indexOf('@')
if (at >= 0) { ... } else { ... }

А можно так — через takeIf + Elvis:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val safeAt = at.takeIf { it >= 0 } ?: -1
    println(safeAt) // -1
}

Окей, это пока не супер полезно: мы просто вернули -1. Полезнее — использовать fallback, который гарантированно безопасен.

Например, иногда удобно говорить: «если "@" нет — считаем, что домена нет»:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val domain = at.takeIf { it >= 0 }
        ?.let { email.substring(it + 1) } // (про let позже, сегодня без него)
        ?: "INVALID"

    println(domain) // INVALID
}

Но стоп. Мы ещё не проходили scope‑функции в этом дне (это будет позже), так что давайте сделаем то же самое без let, честно и понятно:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val domain = if (at.takeIf { it >= 0 } != null) {
        email.substring(at + 1)
    } else {
        "INVALID"
    }

    println(domain) // INVALID
}

Здесь видно важную мысль: takeIf — это не «замена if навсегда», это инструмент, который позволяет превратить “плохое значение” в null.

В официальных примерах Kotlin встречается похожий приём: индекс ищут, затем делают .takeIf { it >= 0 } ?: s.length, чтобы получить безопасную границу.

5. Пример: «пустая строка после trim» — это тоже невалидный ввод

Пользователь ввёл " " (три пробела). Формально это строка не пустая (length = 3), но смысл у неё как у обещаний «начну учиться с понедельника» — то есть ноль.

Частый паттерн: сначала trim(), потом проверка.

takeIf позволяет записать это очень линейно:

fun main() {
    val raw = "   "
    val name = raw.trim().takeIf { it.isNotEmpty() } ?: "UNKNOWN"

    println(name) // UNKNOWN
}

Здесь читается почти как русская фраза: «возьми trimmed‑строку, если она не пустая, иначе UNKNOWN».

6. takeUnless: «то же самое, но наоборот»

takeUnless — зеркальный брат takeIf.

  • takeIf { cond } оставляет значение, когда cond == true
  • takeUnless { cond } оставляет значение, когда cond == false

Зачем он нужен, если можно написать takeIf { !cond }? Потому что иногда инверсия выглядит проще именно как «если НЕ…».

Представим, что в командах нашего CLI мы запрещаем некоторые «зарезервированные слова» для категории, например "all" (потому что команда list all может означать «покажи всё»).

fun main() {
    val rawCategory = "all"

    val category = rawCategory.trim()
        .takeUnless { it.lowercase() == "all" }
        ?: "uncategorized"

    println(category) // uncategorized
}

Читается нормально: «возьми категорию, если это НЕ all, иначе uncategorized».

7. Как это помогает в CLI: аккуратный разбор аргументов

Сейчас будет важный практический момент. В командах вроде:

  • add food 120 coffee
  • remove 3
  • list food

ввод пользователя всегда «грязный»: лишние пробелы, пустые аргументы, странные числа, иногда вообще пустая строка.

Мы уже умеем нормализовать пробелы через Regex("""\s+""").replace(...) (это было в предыдущей лекции). Теперь добавим охранные условия через takeIf/takeUnless, чтобы каждое промежуточное значение либо становилось нормальным, либо превращалось в null.

Нормализуем токен: «строка или null»

Сделаем маленькую функцию‑помощник. Она пригодится практически везде: имя категории, текст заметки, команда.

fun normalizeToken(raw: String): String? {
    val cleaned = raw.trim()
    return cleaned.takeIf { it.isNotEmpty() }
}

И проверим:

fun main() {
    println(normalizeToken("  milk  ")) // milk
    println(normalizeToken("   "))      // null
}

Вывод null здесь — не «плохо», это полезный сигнал: дальше можно сделать ?: и решить, что делать.

Парсим положительную сумму: «число или null»

В трекере расходов сумма должна быть числом, и чаще всего — положительным. С takeIf удобно сделать это без лишних ветвлений:

fun parsePositiveIntOrNull(raw: String): Int? {
    val n = raw.trim().toIntOrNull()
    return n?.takeIf { it > 0 }
}

Проверим:

fun main() {
    println(parsePositiveIntOrNull(" 120 ")) // 120
    println(parsePositiveIntOrNull("-5"))    // null
    println(parsePositiveIntOrNull("abc"))   // null
}

Здесь одновременно работают две идеи: toIntOrNull() даёт null на нечисловом вводе, а takeIf { it > 0 } превращает «число, но неправильное» тоже в null. В итоге у нас единый “не прошло проверку”‑сигнал.

takeIf + Elvis как guard в одну строку

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

Например: пользователь вводит remove id. Если id невалидный — используем 0 как маркер (или можем напечатать ошибку, но сегодня сконцентрируемся на выражении).

fun main() {
    val rawId = " -10 "
    val id = rawId.trim().toIntOrNull()?.takeIf { it >= 1 } ?: 0

    println(id) // 0
}

Это очень похоже на классическую идиому из документации: «взять значение, если оно валидно, иначе подставить длину/границу/дефолт».

8. Мини‑пример: разбор remove через охранные выражения

Сделаем маленький кусок логики для команды remove. Мы не пишем весь проект целиком в лекции, но будем двигать один и тот же стиль.

Сценарий: пользователь вводит строку. Мы хотим получить id: Int?, где null означает “не получилось”.

fun parseRemoveIdOrNull(line: String): Int? {
    val parts = Regex("""\s+""").replace(line.trim(), " ").split(" ")

    if (parts.isEmpty()) return null
    if (parts[0].lowercase() != "remove") return null

    val rawId = parts.getOrNull(1) ?: return null
    return rawId.toIntOrNull()?.takeIf { it >= 1 }
}

Да, здесь есть if. Это нормально. Мы используем takeIf не как религию, а как удобный “мини‑фильтр” на конкретном шаге.

Можно проверить:

fun main() {
    println(parseRemoveIdOrNull("remove 3"))     // 3
    println(parseRemoveIdOrNull("remove -2"))    // null
    println(parseRemoveIdOrNull("remove abc"))   // null
}

Тут полезно заметить: takeIf помогает именно там, где у нас «значение уже есть, но надо проверить его на условие».

9. Как думать про takeIf/takeUnless и где они уместны

Схема мышления: «преврати плохое в null → обработай единым способом»

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

flowchart TD
    A[Сырое значение] --> B[Нормализация: trim/replace]
    B --> C{Проверка условия}
    C -->|OK| D[Значение]
    C -->|FAIL| E[null]
    E --> F[Elvis ?: fallback/сообщение/выход]

Смысл: takeIf/takeUnless — это удобная реализация узла «проверка условия → null при провале».

Когда takeIf/takeUnless улучшают читаемость, а когда мешают

Здесь важно не попасть в типичную ловушку новичка: «о, новая фича, сейчас я её натяну на весь проект». Так обычно появляются PR’ы, после которых коллеги начинают пить ромашковый чай вместо кофе.

takeIf/takeUnless обычно уместны, когда условие короткое и напрямую описывает валидность. Классические примеры: it >= 0, it.isNotEmpty(), it in 1..10. В Kotlin‑примерах можно встретить стиль, где строку превращают в значение и сразу фильтруют по длине через takeIf — это как раз хороший, простой предикат.

Когда же условие длинное, с несколькими проверками и «и ещё вот это, и вот это», обычный if часто читается лучше, потому что вы можете разложить мысль на несколько строк и дать переменным понятные имена.

Подборка мини‑примеров (5–10 строк)

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

Берём строку, только если она “похожа на команду”

fun main() {
    val raw = "   add food 120 coffee   "
    val commandLine = raw.trim().takeIf { it.contains(' ') } ?: ""

    println(commandLine) // add food 120 coffee
}

Отбрасываем слишком короткий токен

fun main() {
    val token = "ok"
    val good = token.takeIf { it.length >= 3 } ?: "too_short"

    println(good) // too_short
}

takeUnless как «запрет значения»

fun main() {
    val category = "root"
    val safe = category.takeUnless { it == "root" } ?: "guest"

    println(safe) // guest
}

10. Типичные ошибки при использовании takeIf/takeUnless

Ошибка №1: забыть, что результат nullable, и обращаться к нему как к обычному значению.
takeIf и takeUnless почти всегда возвращают T?, даже если исходное значение было T. Новичок пишет val x = s.takeIf { ... } и тут же делает x.length. Компилятор справедливо возмущается, а студент — нет. Исправление простое: либо добавляйте Elvis ?:, либо делайте проверку на null, либо продолжайте цепочку через safe call ?..

Ошибка №2: пытаться заменить takeIf-ом целую “ветку бизнес‑логики”.
Если при невалидном вводе нужно печатать сообщение, просить повторить ввод, логировать ошибку или делать разные действия — это уже не “фильтрация значения”, а ветвление поведения. В таких местах if/when обычно честнее и читабельнее: вы не прячете важную логику в одну строку “ради красоты”.

Ошибка №3: злоупотреблять takeUnless, когда читателю проще увидеть takeIf.
takeUnless { it == "all" } может выглядеть нормально, а вот takeUnless { it.length < 3 } уже заставляет мозг делать двойное отрицание. Если фраза “возьми, если …” звучит естественнее, используйте takeIf. Если естественнее “возьми, если НЕ …” — используйте takeUnless.

Ошибка №4: писать слишком сложные условия прямо внутри takeIf { ... }.
Когда предикат разрастается до «три условия, два &&, одно || и ещё скобки», код перестаёт быть обучаемым. В этом случае лучше вынести проверку в отдельную функцию с говорящим именем (например, isValidCategoryName(s)), или хотя бы сделать промежуточные val, а уже потом применять takeIf.

Ошибка №5: путать “маркер невалидности” и “реальный смысл null”.
В нашей теме null — это удобный технический сигнал “не прошло проверку”. Но дальше по курсу вы будете проектировать типы так, чтобы null не означал «всё подряд». Поэтому старайтесь, чтобы null в вашем коде был либо явно ожидаемым результатом (parse...OrNull), либо осознанным “флажком” от takeIf/takeUnless, который тут же обрабатывается через ?: или проверку.

1
Задача
Kotlin SELF, 27 уровень, 4 лекция
Недоступна
Приветствие гостя
Приветствие гостя
1
Задача
Kotlin SELF, 27 уровень, 4 лекция
Недоступна
Проверка оценки
Проверка оценки
1
Задача
Kotlin SELF, 27 уровень, 4 лекция
Недоступна
Домен из адреса
Домен из адреса
1
Задача
Kotlin SELF, 27 уровень, 4 лекция
Недоступна
Категория без запрета
Категория без запрета
1
Опрос
Строки углублённо, 27 уровень, 4 лекция
Недоступен
Строки углублённо
Строки углублённо
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ