JavaRush /Курсы /Kotlin SELF /Валидация через when: in/!in, диапазоны

Валидация через when: in/!in, диапазоны

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

1. when как инструмент валидации

Зачем нужна валидация

Если вы когда‑нибудь вводили «возраст = 999» или «оценка = -12» и программа это «проглотила», вы уже знакомы с болью отсутствия валидации. Валидация — это слой, который отделяет «программа принимает данные» от «программа верит данным». И вот здесь when неожиданно становится не просто ветвлением, а маленькой таблицей правил: какие значения допустимы, какие — нет, какие требуют отдельного сообщения.

Представьте, что вам нужно проверить число и отнести его к категории: «0..12», «13..17», «18..120», иначе «ошибка». Если писать это на if, получится цепочка, которая читается как юридический документ. when же читается как аккуратная шкала.

Диапазоны и in / !in

Диапазон в Kotlin — это объект вида a..b, который означает «все значения от a до b включительно». Слово «включительно» очень важно: 0..10 содержит и 0, и 10. А оператор in — это проверка «принадлежит ли значение диапазону». Соответственно, !in — «не принадлежит».

Пока это звучит как математика из школы, но в коде это превращается в очень понятные фразы: age in 0..120 читается буквально как «возраст в диапазоне от 0 до 120». А теперь главное: в when можно писать ветки не только для конкретных значений, но и для проверок принадлежности диапазону.

Небольшой разогрев:


fun main() {
    val x = 7

    println(x in 1..10)   // true
    println(x !in 1..10)  // false
}

Тут нет магии: in возвращает Boolean, и этим Boolean удобно управлять логикой программы.

Валидация Int по диапазонам

Когда число нужно не просто «принять/отклонить», а разложить по категориям, when с диапазонами оказывается почти идеальным. Вы пишете шкалу сверху вниз, и программа выбирает первую подходящую ветку. Для новичка это приятно тем, что код выглядит как «правила на бумаге»: сначала дети, потом подростки, потом взрослые, иначе ошибка.

Начнём с мини‑функции, которая классифицирует возраст. Обратите внимание: это именно валидация + интерпретация: мы не просто говорим «плохо/хорошо», мы возвращаем осмысленный результат.

fun ageCategory(age: Int): String {
    return when (age) {
        in 0..12 -> "ребёнок"
        in 13..17 -> "подросток"
        in 18..120 -> "взрослый"
        else -> "некорректный возраст"
    }
}

fun main() {
    println(ageCategory(10))   // ребёнок
    println(ageCategory(17))   // подросток
    println(ageCategory(200))  // некорректный возраст
}

Здесь вы видите сразу три важных вещи. Во‑первых, in 0..12 — это «условие ветки» в when. Во‑вторых, ветки идут сверху вниз, и срабатывает первая подходящая. В‑третьих, else — наша страховка на случай «всего остального».

Ранняя защита через !in

Есть очень практичный стиль валидации: сначала мы проверяем, что число вообще похоже на правду, и только потом делим его на категории. В when это удобно делать через !in ... в самой верхней ветке. Это выглядит как охранник на входе: «с паспортом — проходи, без паспорта — до свидания, и неважно, какой у тебя стиль одежды».

Пример с температурой (тут диапазон условный, просто чтобы увидеть идею):

fun temperatureLabel(t: Int): String {
    return when (t) {
        !in -50..60 -> "вне разумного диапазона"
        in -50..0 -> "холодно"
        in 1..25 -> "нормально"
        else -> "жарко"
    }
}

fun main() {
    println(temperatureLabel(10))   // нормально
    println(temperatureLabel(200))  // вне разумного диапазона
}

Почему это удобно? Потому что вы явно отделяете «плохие данные» от «хороших данных». И потом не приходится думать, куда «впихнуть» -999 или 999 в вашу шкалу.

Диапазоны для Char

В реальных задачах вы будете валидировать не только числа, но и формат ввода. Даже до регулярных выражений (которые будут позже) можно делать много полезного, просто проверяя символы. Например, «является ли символ цифрой», «латинская ли буква», «заглавная или строчная». И тут внезапно выясняется, что диапазоны работают и для Char.

Это особенно полезно, когда вы берёте «первый символ ответа» и ожидаете что‑то вроде Y/N, A/B/C или 1/2/3.

fun charKind(ch: Char): String {
    return when (ch) {
        in '0'..'9' -> "цифра"
        in 'a'..'z' -> "строчная латинская буква"
        in 'A'..'Z' -> "заглавная латинская буква"
        else -> "другой символ"
    }
}

fun main() {
    println(charKind('7'))  // цифра
    println(charKind('Q'))  // заглавная латинская буква
    println(charKind('?'))  // другой символ
}

Важно понимать, что диапазон '0'..'9' — это не «магия про ASCII», а просто порядок символов в Unicode, который устроен так, что цифры идут подряд. Для учебных и практических CLI‑вводов этого более чем достаточно.

2. Группировка и комбинирование правил

Группировка значений через запятую

Иногда валидация — это не диапазоны, а набор отдельных допустимых значений. Например: «команда может быть только 1, 2 или 3», «месяц может быть 1..12», «для этих месяцев 31 день, для этих 30». Если писать отдельную ветку на каждое значение, вы быстро почувствуете себя принтером, который печатает одинаковые строки.

В when можно перечислять несколько значений в одной ветке через запятую. Это не только экономит строки — это делает правило более очевидным: «эти значения — одна группа».

Пример (дни в месяце, без високосных тонкостей — не обижайте февраль, он и так старается):

fun daysInMonth(month: Int): Int {
    return when (month) {
        1, 3, 5, 7, 8, 10, 12 -> 31
        4, 6, 9, 11 -> 30
        2 -> 28
        else -> 0
    }
}

fun main() {
    println(daysInMonth(11)) // 30
    println(daysInMonth(2))  // 28
    println(daysInMonth(99)) // 0
}

Здесь else -> 0 — тоже форма валидации: мы возвращаем «сигнал ошибки» числом. Не идеально (лучше было бы отдельное сообщение), но на текущем уровне курса это честный и понятный способ показать: «месяц некорректный».

Диапазоны и отдельные значения в одном when

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

Смысл в том, что when позволяет вам расположить ветки так, чтобы частные случаи шли выше, а широкие диапазоны — ниже. И это читается почти как приоритет правил: «если ровно 100 — вот так, иначе если в 90..99 — вот так…».

fun scoreComment(score: Int): String {
    return when (score) {
        !in 0..100 -> "некорректный результат"
        100 -> "идеально! (и да, можно чуть погордиться)"
        in 90..99 -> "отлично"
        in 70..89 -> "хорошо"
        else -> "есть над чем поработать"
    }
}

fun main() {
    println(scoreComment(100)) // идеально! (и да, можно чуть погордиться)
    println(scoreComment(105)) // некорректный результат
}

Обратите внимание на порядок. Если бы мы поставили in 90..100 выше, то 100 «поглотилось» бы этой веткой, и отдельное сообщение для 100 никогда бы не сработало.

Мини‑сценарий: настройки консольного приложения

Теперь соберём всё в маленький цельный фрагмент. Представим, что мы делаем консольное приложение “Study Helper”, которое спрашивает у пользователя «уровень сложности» (1..3) и «процент правильных ответов» (0..100). Наша задача — не дать программе молча принять мусор и при этом выдать понятные сообщения.

Ни коллекции, ни сложную архитектуру мы не трогаем: только readln(), trim(), toIntOrNull() и when.

fun difficultyLabel(level: Int): String {
    return when (level) {
        1 -> "легко"
        2 -> "нормально"
        3 -> "сложно"
        else -> "некорректный уровень"
    }
}

fun main() {
    print("Уровень сложности (1-3): ")
    val level = readln().trim().toIntOrNull()

    val message = when (level) {
        null -> "Это не число"
        in 1..3 -> "Ок, выбрано: ${difficultyLabel(level)}"
        else -> "Число есть, но нужно 1..3"
    }

    println(message)
}

Обратите внимание на спокойную «лесенку». Сначала мы проверяем null (не получилось распарсить), потом проверяем диапазон, и только потом всё остальное. Такой порядок почти всегда даёт самое человеческое сообщение: пользователь понимает, что именно он сделал не так.

Пересечения диапазонов и порядок веток

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

Посмотрим на пример, где ошибка почти незаметна:

fun badGrade(score: Int): String {
    return when (score) {
        in 0..100 -> "какой-то результат"
        in 90..100 -> "отлично"
        else -> "ошибка"
    }
}

fun main() {
    println(badGrade(95)) // какой-то результат
}

Почему так? Потому что 95 уже попало в 0..100, а до ветки 90..100 программа даже не дошла.

Правильный вариант — переставить ветки:

fun goodGrade(score: Int): String {
    return when (score) {
        in 90..100 -> "отлично"
        in 0..89 -> "какой-то результат"
        else -> "ошибка"
    }
}

fun main() {
    println(goodGrade(95)) // отлично
}

Если вы запомните только одну мысль из этой лекции, пусть это будет она: в when порядок веток — часть логики, а не просто «красивое оформление».

3. Шпаргалки и визуализация

Таблица‑шпаргалка по валидации в when

Когда вы пишете валидацию, полезно держать пару «формул» в голове. Ниже — маленькая шпаргалка по самым ходовым конструкциям. Она не заменит понимание, но сэкономит время, когда вы в сотый раз пишете проверку «0..100».

Что хотим проверить Как пишем в when (x) Как это читается
Принадлежность диапазону
in 0..100 -> ...
«x от 0 до 100»
Не принадлежит диапазону
!in 0..100 -> ...
«x не от 0 до 100»
Несколько конкретных значений
1, 3, 5 -> ...
«x равен 1 или 3 или 5»
Отдельный «особый случай»
100 -> ...
«если ровно 100 — особая логика (ставим выше диапазона)»
Всё остальное
else -> ...
«любые другие значения»

Заметьте, что Kotlin поощряет использование when именно как выражения, когда вы хотите получить результат, а не просто печатать в каждой ветке. Это и про читаемость, и про дисциплину кода.

Блок‑схема конвейера валидации

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

flowchart TD
    A["readln(): сырая строка"] --> B["trim(): убрали пробелы"]
    B --> C["toIntOrNull(): число или null"]
    C --> D{"when (value)"}
    D -->|null| E["Сообщение: 'Это не число'"]
    D -->|in диапазоне| F["Ок: работаем дальше"]
    D -->|else| G["Сообщение: 'Число есть, но не подходит'"]

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

4. Типичные ошибки при валидации через when

Ошибка №1: пересекающиеся диапазоны, где широкая ветка стоит выше узкой.
Это тот самый случай, когда вы уверены, что «всё описал», но часть логики никогда не выполняется. Если у вас есть ветка in 0..100, то все более узкие диапазоны внутри неё должны стоять выше, иначе они станут недостижимыми по смыслу.

Ошибка №2: забытая граница из‑за “включительности” ...
Новички часто мыслят диапазон как «до, но не включая», особенно если раньше видели такие диапазоны в других языках или в математических обозначениях. В Kotlin 0..10 включает 10, поэтому ошибки “на 1” случаются регулярно. Лечится просто: проговаривайте крайние значения и проверяйте их отдельно в голове.

Ошибка №3: отсутствие осмысленного else или попытка обойтись без него.
В валидации else — это не «чтобы компилятор отстал», а обязательная ветка «что делать с неправильными значениями». Если else возвращает что‑то бессмысленное вроде пустой строки, вы прячете ошибку вместо того, чтобы сообщить о ней. Потом это выстрелит в неожиданный момент.

Ошибка №4: смешивание в одной ветке вычислений, печати и изменения состояния.
Когда when используется для валидации, он особенно хорош как выражение: вернули сообщение/статус/число, а печать сделали отдельно. Если в ветках начинается «и распечатать, и изменить переменную, и ещё раз прочитать ввод», ваш when превращается в мини‑сериал на 12 сезонов, который сложно тестировать и читать.

Ошибка №5: нет проверки null после toIntOrNull().
Если вы распарсили строку через toIntOrNull(), то null — это нормальный исход, а не исключение. Часто самая понятная структура — отдельная ветка null -> "Это не число", а уже потом диапазоны. Так пользователь получит точное объяснение, а вы не будете бороться с компилятором и nullable‑типами.

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