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 — це розширювальна функція, яку викликають на самому значенні. Вона повертає:

  • це саме значення, якщо умова істинна,
  • 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 пізніше, сьогодні без нього)
        ?: "НЕКОРЕКТНО"

    println(domain) // НЕКОРЕКТНО
}

Але стоп. Ми ще не проходили 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 {
        "НЕКОРЕКТНО"
    }

    println(domain) // НЕКОРЕКТНО
}

Тут добре видно важливу думку: 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() } ?: "НЕВІДОМО"

    println(name) // НЕВІДОМО
}

Це читається майже як українська фраза: «візьми рядок після trim, якщо він не порожній, інакше — НЕВІДОМО».

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" }
        ?: "без_категорії"

    println(category) // без_категорії
}

Читається природно: «візьми категорію, якщо це НЕ all, інакше — без_категорії».

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 } ?: "занадто_коротко"

    println(good) // занадто_коротко
}

takeUnless як «заборона значення»

fun main() {
    val category = "root"
    val safe = category.takeUnless { it == "root" } ?: "гість"

    println(safe) // гість
}

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
Опитування
Рядки поглиблено, рівень 27, лекція 4
Недоступний
Рядки поглиблено
Рядки поглиблено
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ