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) | Как это читается |
|---|---|---|
| Принадлежность диапазону | |
«x от 0 до 100» |
| Не принадлежит диапазону | |
«x не от 0 до 100» |
| Несколько конкретных значений | |
«x равен 1 или 3 или 5» |
| Отдельный «особый случай» | |
«если ровно 100 — особая логика (ставим выше диапазона)» |
| Всё остальное | |
«любые другие значения» |
Заметьте, что 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‑типами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ