1. Диапазоны в switch
Когда вы впервые пишете проверку «попадает ли число в интервал», почти всегда получается что-то вроде x >= 0 && x <= 100. Это нормально, но ровно до того момента, пока таких проверок не станет десять. Тогда код превращается в набор магических чисел, где легко ошибиться в границе и потом героически чинить баг «почему 100 не считается отличным баллом».
Диапазоны в switch решают эту проблему: вы начинаете писать правила так, как вы их проговариваете вслух. «Если от 0 до 59 — провал», «если от 60 до 89 — нормально», «если от 90 до 99 — отлично», «если 100 — идеально». Это не только короче, но и заметно читабельнее.
Для примера сегодня мы будем развивать мини‑приложение TicketBot — консольный помощник для расчёта категории билета (детский/взрослый/льготный) и простых скидок. Пока без функций и без коллекций: всё на top‑level коде, как вы уже делали раньше.
Диапазон как паттерн: case 0..<10
Когда switch сравнивает значение с case, он не обязан сравнивать только с конкретным числом. В Swift case — это «паттерн», то есть шаблон совпадения. Таким паттерном может быть диапазон. Вы пишете case 0..<10: — и это читается как «значение попадает в диапазон от 0 до 9».
Важно уловить мысль: здесь мы не пишем условие в стиле if. Мы задаём шаблон, которому значение либо соответствует, либо нет. Это очень похоже на сортировку писем по ящикам: вы не пересчитываете каждое письмо заново, вы просто кладёте его в правильный слот по правилу.
Простой пример с «оценкой по баллам»:
import Foundation
let score = 97
switch score {
case 90..<100:
print("Отлично, но не максимум") // Отлично, но не максимум
case 0..<90:
print("Есть куда расти") // (не выполнится)
default:
print("Некорректный балл") // (не выполнится)
}
Здесь 90..<100 означает «90, 91, …, 99». Значение 100 туда не входит.
2. Границы и порядок case
... против ..<
Когда речь о диапазонах, самая частая ошибка новичка — перепутать «включительно» и «не включительно». Это не мелочь: одна неправильная точка может превратить 18-летнего пользователя в «несовершеннолетнего». Поэтому сейчас мы притормозим и закрепим границы спокойно, без спешки.
Сделаем небольшую таблицу, которую полезно держать в голове:
| Запись диапазона | Название | Правая граница | Пример чтения |
|---|---|---|---|
|
закрытый диапазон | включается | «от a до b включительно» |
|
полуоткрытый диапазон | не включается | «от a до (b-1) включительно» |
Теперь пример, где границы реально важны. Мы хотим классифицировать баллы так, чтобы 100 обрабатывался отдельно:
import Foundation
let score = 100
switch score {
case 100:
print("Идеально!") // Идеально!
case 90..<100:
print("Почти идеально")
case 60..<90:
print("Нормально")
case 0..<60:
print("Провал")
default:
print("Некорректное значение")
}
Обратите внимание на очень полезный приём: мы ставим case 100: выше, а затем используем 90..<100, чтобы диапазоны не пересекались. Такой код легко проверять глазами: 0…59, 60…89, 90…99, 100.
Односторонние диапазоны: ..<0
Иногда вы хотите обработать «все значения меньше некоторого порога» или «все значения больше/равно порога». Писать для этого отдельные диапазоны с огромными числами — сомнительное удовольствие (и да, -999999999...-1 звучит как заклинание из фэнтези).
Swift позволяет использовать односторонние диапазоны прямо в case. Например, case ..<0: означает «всё, что меньше нуля». Это удобно для валидации вводимых чисел: сначала ловим совсем неправильные значения, а потом работаем с нормальными.
Пример для TicketBot: пользователь вводит возраст, а мы сразу отсеиваем «минус пять лет».
import Foundation
let age = Int(readLine() ?? "") ?? -1
switch age {
case ..<0:
print("Возраст не может быть отрицательным") // если ввели -5
case 0...120:
print("Возраст принят: \(age)")
default:
print("Слишком фантастический возраст")
}
Такой switch хорош тем, что он читается как «проверка здравого смысла». И да, ..<0 выглядит как смайлик, который грустит, потому что возраст отрицательный.
Почему важен порядок case
На предыдущей лекции вы уже видели, что switch идёт сверху вниз и срабатывает первый подходящий case. Сейчас это становится ещё важнее, потому что диапазоны могут пересекаться. Если поставить «широкий» диапазон выше «узкого», узкий никогда не сработает — его уже «поймали» раньше.
Давайте специально сделаем пример с ошибкой и посмотрим, почему он логически плох:
import Foundation
let score = 95
switch score {
case 0..<100:
print("Любой валидный балл") // сработает это
case 90..<100:
print("Отлично") // а сюда мы никогда не попадём
default:
print("Некорректно")
}
Правильная мысль здесь простая: сначала ставим более конкретные правила, потом — более общие. Исправим порядок:
import Foundation
let score = 95
switch score {
case 90..<100:
print("Отлично") // Отлично
case 0..<90:
print("Обычный балл")
default:
print("Некорректно")
}
Если держать это правило в голове, вы будете ловить половину багов ещё до запуска программы: просто глазами.
3. where в case
Дополнительное условие через where
Иногда одного диапазона мало. Например, вы хотите написать правило: «положительное число и при этом чётное». Можно сделать вложенный if внутри case, но тогда switch начинает распухать и теряет свою прелесть «плоской» структуры.
Для таких ситуаций в Swift у case есть уточнение where: дополнительное условие, которое должно быть истинным, чтобы ветка считалась подходящей. В результате вы пишете правило в одном месте, не плодите вложенности и не превращаете код в матрёшку.
Классический пример — различить чётные и нечётные среди положительных:
import Foundation
let x = 12
switch x {
case let v where v > 0 && v.isMultiple(of: 2):
print("Положительное чётное: \(v)") // Положительное чётное: 12
case let v where v > 0:
print("Положительное нечётное: \(v)")
case 0:
print("Ноль")
default:
print("Отрицательное")
}
Здесь используется isMultiple(of:) как понятный способ проверить кратность.
Зачем почти всегда нужен case let v
На первый взгляд может показаться, что писать case let v странно. Ведь у нас уже есть переменная x. Почему бы не использовать её?
Ответ становится понятнее, если вспомнить, как работает switch. Когда вы пишете switch x, значение x передаётся внутрь механизма сопоставления с паттернами. Дальше switch сравнивает это значение с каждым case. Но внутри ветки case вы работаете уже не с исходной переменной, а с тем значением, которое совпало с паттерном.
Это особенно заметно, когда в switch передаётся не переменная, а выражение. Например, можно сделать даже так:
import Foundation
switch Int(readLine() ?? "") ?? 0 {
case let v where v > 0:
print("Положительное число: \(v)")
default:
print("Ноль или отрицательное")
}
Здесь в switch передано выражение Int(readLine() ?? "") ?? 0. У него нет имени. После того как switch получил результат этого выражения, обратиться к нему напрямую уже нельзя — переменной для этого просто не существует. Поэтому в case мы создаём имя для совпавшего значения. Запись case let v говорит компилятору: если значение подошло под этот case, привяжи его к имени v, чтобы я мог использовать его внутри ветки.
После такой привязки значение становится обычной переменной. Его можно проверять, передавать в функции или выводить на экран. Именно поэтому условие в примере выглядит так: case let v where v > 0. Сначала мы связываем совпавшее значение с именем v, а затем уточняем правило через where.
4. Практика: TicketBot
Теперь давайте соберём это в небольшой, но практичный пример TicketBot. Мы будем спрашивать возраст и час посещения (0…23), а затем определять категорию и скидку «ранняя птица» до 10 утра. Цель не в том, чтобы сделать идеальную кассу кинотеатра, а в том, чтобы увидеть: диапазоны — это классификация, where — это уточнение правила.
Сначала нормализуем ввод и заведём пару констант‑диапазонов, чтобы не повторять числа:
import Foundation
let age = Int(readLine() ?? "") ?? -1
let hour = Int(readLine() ?? "") ?? -1
let validAge = 0...120
let validHour = 0...23
Теперь основной switch по возрасту. Обратите внимание: в case можно использовать переменную‑диапазон (у нас validAge, validHour), потому что диапазон — это значение, а case умеет матчить по паттерну‑диапазону.
import Foundation
let age = Int(readLine() ?? "") ?? -1
let hour = Int(readLine() ?? "") ?? -1
let validAge = 0...120
let validHour = 0...23
if !validAge.contains(age) || !validHour.contains(hour) {
print("Некорректный ввод")
} else {
switch age {
case 0..<7:
print("Категория: малыш")
case 7..<18:
print("Категория: детский")
case 18..<65:
print("Категория: взрослый")
default:
print("Категория: льготный")
}
}
Пока мы сделали только классификацию. Теперь добавим скидку по времени. Можно было бы городить второй switch отдельно по hour, но мы потренируемся использовать where и сделаем «взрослый билет утром дешевле».
Идея такая: в ветке «взрослый» мы добавим уточняющую ветку раньше, чем обычную взрослую. Это как раз тот случай, когда порядок case и where работают вместе.
import Foundation
let age = Int(readLine() ?? "") ?? -1
let hour = Int(readLine() ?? "") ?? -1
let validAge = 0...120
let validHour = 0...23
if !validAge.contains(age) || !validHour.contains(hour) {
print("Некорректный ввод")
} else {
switch age {
case let a where (18..<65).contains(a) && hour < 10:
print("Категория: взрослый (скидка за ранний сеанс)") // если hour = 9
case 0..<7:
print("Категория: малыш")
case 7..<18:
print("Категория: детский")
case 18..<65:
print("Категория: взрослый")
default:
print("Категория: льготный")
}
}
Заметьте, что в where мы используем hour, который лежит снаружи. Это нормально: where — обычное условие, оно видит переменные из области видимости.
И ещё одно важное наблюдение: мы намеренно не тащим сюда будущие темы вроде функций или пользовательских типов. Да, TicketBot пока выглядит «простынёй», но на текущем этапе это честная простыня: всё, что в ней есть, вы уже умеете читать.
5. Типичные ошибки при switch с диапазонами и where
Ошибка №1: перепутать ... и ..< и потерять граничное значение.
Это классика жанра: вы хотели «до 18 включительно», написали 0..<18 и внезапно 18 оказалось «взрослым». Лечится просто: для каждого диапазона мысленно проверяйте правую границу, а ещё лучше — прогоняйте в голове 2–3 тестовых значения: левую границу, правую границу и значение «сразу после».
Ошибка №2: пересечь диапазоны и удивляться, почему срабатывает «не та ветка».
Если у вас есть case 0..<100: и ниже case 90..<100:, то «отлично» никогда не случится, потому что 95 уже попало в верхний case. Это не баг Swift, это честное следование порядку: switch идёт сверху вниз. Лекарство — сначала ставить более узкие правила, потом более широкие.
Ошибка №3: сделать where слишком тяжёлым и перестать понимать собственный код.
where соблазняет: «О, я сейчас напишу одно гигантское условие и всё решу». Потом вы возвращаетесь к коду через два дня и чувствуете себя археологом. Держите правило: одна ветка — одна мысль. Если условие стало слишком длинным, лучше разбить логику на несколько case или хотя бы упростить проверку через заранее подготовленные диапазоны.
Ошибка №4: забыть привязку значения в case let v where ... и пытаться использовать v.
where сам по себе не создаёт имя. Если вы написали что-то в духе case 0..<100 where v.isMultiple(of: 2), компилятор справедливо спросит: «а кто такой v?». Чтобы использовать значение, его нужно связать: case let v where ... Такая привязка — нормальная часть паттернов switch.
Ошибка №5: пытаться писать в case «условие как в if», например case x > 0:.
Это популярный рефлекс после if. В switch так нельзя: case — это паттерн (литерал, диапазон, _, и так далее), а для «условной логики» как раз существует where. Если хочется «больше нуля», пишем case let v where v > 0: и радуемся, что получилось читаемо.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ