1. Отличная вещь
Когда мы пишем программу, мы почти всегда работаем с ограничениями: возраст должен быть от 0 до 120, оценка — от 0 до 100, время суток — от 0 до 23, пароль — хотя бы 8 символов, а количество попыток — не больше трёх. В коде эти ограничения часто превращаются в длинные условия и повторяющиеся числа.
Проблема в том, что границы легко перепутать. Можно забыть, включается ли 18 в “взрослый” возраст, можно ошибиться на единицу (привет, классическая off-by-one), можно разъехаться в разных местах программы: в одном месте вы проверили <= 100, а в другом случайно < 100. Диапазоны в Swift — это способ выразить “правило границ” компактно и одинаково везде.
2. Диапазон как объект: ... и ..<
Если очень по‑человечески, диапазон — это “отрезок” чисел между двумя границами. Swift позволяет записывать диапазоны буквально так, как вы бы сказали вслух: “от 1 до 5 включительно” или “от 0 до 10, но 10 не включаем”. Отсюда и две главные формы: ... и ..<.
Закрытый диапазон: a...b
Закрытый диапазон включает обе границы: и a, и b.
let levels = 1...5
print(levels.contains(1)) // true
print(levels.contains(5)) // true
print(levels.contains(6)) // false
Здесь 1 входит, 5 входит, 6 — уже нет. Это прям “школьная математика”.
Полуоткрытый диапазон: a..<b
Полуоткрытый диапазон включает левую границу, но не включает правую.
let digits = 0..<10
print(digits.contains(0)) // true
print(digits.contains(9)) // true
print(digits.contains(10)) // false
Это крайне удобно для “количественных” вещей. Например, если у вас n элементов, то индексы часто идут от 0 до n-1, то есть 0..<n.
Даже если вы пока не проходили массивы, сама идея “n значений” встречается постоянно: “повтори n раз”, “сделай попытки от 0 до n-1”, “пройди часы от 0 до 23”.
Мини‑таблица: как читать ... и ..<
| Запись | Как читается по‑русски | Включает правую границу? |
|---|---|---|
|
“от a до b включительно” | Да |
|
“от a до b, но b не включаем” | Нет |
Чтобы закрепить разницу, полезно мысленно проверять “крайние значения”: подставьте b и спросите себя — оно должно входить или нет?
3. Диапазон как правило: храним в let и переиспользуем
До этого момента вы, скорее всего, воспринимали диапазон как “штуку в for”. Но на самом деле диапазон — это отдельное значение, которое можно сохранить в переменную/константу и применять много раз. Это похоже на то, как мы сохраняем число в let maxAttempts = 3 вместо того, чтобы писать 3 в десяти местах.
Представим, что мы пишем маленькую консольную программу “Вход в клуб”.
let adultAgeRange = 18...120
let age = Int(readLine() ?? "") ?? 0
if adultAgeRange.contains(age) {
print("Добро пожаловать! Вам \(age).")
} else {
print("Извините, вход только с 18 лет.")
}
Здесь важна идея: число 18 и 120 теперь живут в одном месте — в диапазоне. Если правила изменятся, вы меняете диапазон, а не переписываете условия по всему коду.
Такой подход особенно полезен, когда вы валидируете ввод пользователя. У пользователя талант вводить странные вещи: -10, 999, пустую строку, “двадцать”. Мы не можем запретить ему, но можем аккуратно проверить.
4. Диапазоны в for-in: границы цикла без сюрпризов
Вы уже умеете писать циклы, и диапазоны — это основной “топливный шланг” для for-in, когда вы перебираете числа. Сегодня мы просто делаем этот механизм осознанным: вы будете выбирать ... или ..< не наугад, а потому что так правильно по смыслу.
“Сделать n раз”: почти всегда 0..<n
Если вам нужно выполнить действие ровно n раз, удобно идти от 0 до n-1.
let n = 5
for i in 0..<n {
print("Итерация #\(i)")
}
// Итерация #0
// Итерация #1
// Итерация #2
// Итерация #3
// Итерация #4
Почему это так популярно? Потому что “количество” — это n, а индексирование (и вообще счёт “с нуля”) в программировании встречается постоянно.
“От 1 до n включительно”: удобно 1...n
Если у вас “человеческий” счёт: первый, второй, третий… и вы хотите включить n, берите закрытый диапазон.
let n = 5
for step in 1...n {
print("Шаг \(step)")
}
// Шаг 1
// Шаг 2
// Шаг 3
// Шаг 4
// Шаг 5
Блок‑схема выбора диапазона
flowchart TD
A["Нужно повторить ровно n раз?"] -->|Да| B["0..<n"]
A -->|Нет| C["Нужны 'человеческие' числа 1..n?"]
C -->|Да| D["1...n"]
C -->|Нет| E["Подумай о границах: правая включается?"]
Смысл простой: сперва определяем задачу, а потом выбираем синтаксис. Не наоборот.
Пустой диапазон и пограничные случаи
В программировании часто встречается мысль: “если границы одинаковые — значит одно значение”. Для закрытого диапазона это правда: 5...5 содержит ровно одно число — 5.
А вот полуоткрытый диапазон 5..<5 содержит ничего, то есть является пустым. Это логично: мы включаем левую границу, но исключаем правую, а они одинаковые — значит, не осталось ни одного значения.
Это полезно, потому что пустой диапазон отлично ведёт себя в цикле: он просто не выполняется ни разу.
for i in 5..<5 {
print(i) // этот код не выполнится ни разу
}
print("Готово") // Готово
Практический смысл такой: если “количество” оказалось нулевым, 0..<0 — это честное “выполнить 0 раз”. Без специальных if.
5. stride(from:to:by:): когда шаг не равен 1
Циклы по диапазону идут с шагом 1. Но иногда нам нужно “прыгать” по числам: только чётные, только каждое третье, обратный отсчёт, “покажи 10, 8, 6, 4, 2, 0”… Вот здесь появляется stride.
Идея stride: “иди от start к end, делая шаг by”
В Swift есть функция stride(from:to:by:), которая создаёт последовательность чисел с заданным шагом. Важно понимать семантику: вариант с to: даёт значения на полуоткрытом интервале [start, end), то есть не достигает end.
Пример: выведем чётные числа меньше 10.
for i in stride(from: 0, to: 10, by: 2) {
print(i)
}
// 0
// 2
// 4
// 6
// 8
Если вам нужно “до 10 включительно”, у stride есть сосед — stride(from:through:by:). Он работает как [start, end] (правая граница потенциально включается), но с важной оговоркой: конец включится только если он достижим по шагу.
Отрицательный шаг: идём вниз
stride умеет ходить назад — просто задаём отрицательный by:.
for t in stride(from: 10, to: 0, by: -2) {
print(t)
}
// 10
// 8
// 6
// 4
// 2
Обратите внимание: to: 0 не включается, поэтому 0 не напечатается. Если вам принципиально вывести и 0, используйте through: 0.
Очень важное правило: by: не должен быть нулём
Шаг by: 0 — это логическая катастрофа: вы никуда не двигаетесь, а цикл потенциально становится бесконечным. Поэтому шаг должен быть конечным и ненулевым.
Для новичка это хорошая мысль: если вы задаёте “шаг”, убедитесь, что он действительно шаг, а не “стояние на месте”.
Осторожно с дробными шагами
На целых числах stride почти всегда ведёт себя предсказуемо. А вот на дробных (Double) может появляться накопление ошибок из‑за особенностей floating‑point арифметики: вы можете ожидать 1.0, 1.1, 1.2 и т. д., а получить значения, которые отличаются на микроскопическую погрешность.
Мы сейчас не углубляемся в допуски и сравнение Double, просто запоминаем как дорожный знак: “с дробями будь внимательнее”.
6. Где применять: мини‑приложение «Терминал кинотеатра»
Сейчас мы соберём небольшой консольный сценарий: пользователь вводит возраст и время сеанса, а программа проверяет, можно ли продать билет. Параллельно мы покажем, как stride помогает выводить “каждое второе место” (например, чтобы посадить людей через одно).
Мы не используем массивы, функции и switch — только то, что у вас уже есть, плюс диапазоны и stride.
Диапазоны как правила: возраст и часы
Сначала заведём правила:
- допустимый возраст: 0...120
- взрослый: 18...120
- время сеанса: 0..<24 (часы суток)
let validAge = 0...120
let adultAge = 18...120
let validHour = 0..<24
print("Введите возраст:")
let age = Int(readLine() ?? "") ?? -1
print("Введите час сеанса (0-23):")
let hour = Int(readLine() ?? "") ?? -1
if !validAge.contains(age) || !validHour.contains(hour) {
print("Ошибка ввода: проверьте возраст и час сеанса.")
} else {
if adultAge.contains(age) {
print("Билет можно продать. Вам \(age), сеанс в \(hour):00.")
} else {
print("Вы несовершеннолетний. Проверьте ограничения фильма.")
}
}
Заметьте, как читается validHour.contains(hour). Это почти обычная русская фраза: “валидный диапазон часов содержит введённый час”.
И в отличие от hour >= 0 && hour < 24 вы не рискуете забыть, где <=, а где <.
stride для мест “через одно”
Допустим, у нас зал с местами от 1 до 20, и мы хотим предложить пользователю места через одно: 1, 3, 5, ... Это выглядит как идеальная работа для stride.
print("Доступные места (через одно):")
for seat in stride(from: 1, to: 21, by: 2) {
print("Место #\(seat)")
}
Почему to: 21, а не to: 20? Потому что to: не включает правую границу. Мы хотим включить 19 (и при этом 21 нам не нужен), поэтому “верхнюю границу” удобно ставить на 1 больше, чтобы 20 точно не попало, а 19 попало.
Небольшая склейка сценария
Теперь соединим всё в один короткий сценарий: если ввод корректный — показываем места.
let validAge = 0...120
let validHour = 0..<24
print("Возраст:")
let age = Int(readLine() ?? "") ?? -1
print("Час сеанса (0-23):")
let hour = Int(readLine() ?? "") ?? -1
if validAge.contains(age) && validHour.contains(hour) {
print("Ок, сеанс в \(hour):00. Возможные места:")
for seat in stride(from: 1, to: 21, by: 2) {
print(seat, terminator: " ")
}
print() // перевод строки
} else {
print("Некорректные данные. Попробуйте ещё раз.")
}
Тут мы ещё и применили опыт про форматирование вывода: печатаем места в одну строку через terminator: " ", а потом делаем print() для переноса строки.
7. Типичные ошибки: диапазоны и stride
Ошибка №1: перепутать ... и ..< на правой границе.
Самая частая ловушка — когда вы думаете “до 10”, пишете 0...10, а потом удивляетесь, что получилось 11 значений. Или наоборот: вам нужно включить 23 (часы суток), а вы пишете 0..<23 и тихо теряете последний час. Лечится привычкой проверять крайние значения: “входит ли правая граница или нет?”.
Ошибка №2: пытаться делать “n раз” через 0...n.
Диапазон 0...n содержит n + 1 значений, и в цикле вы получите на одну итерацию больше. Это почти всегда случайная ошибка. Для “n раз” чаще всего подходит 0..<n, потому что он даёт ровно n шагов.
Ошибка №3: ожидать, что 5..<5 даст одно значение.
Полуоткрытый диапазон при равных границах пустой. Это не баг, а полезная логика: “ноль значений” честно превращается в “ноль итераций”. Если вам нужно одно значение, используйте 5...5.
Ошибка №4: задать stride(..., by: 0) или шаг не в ту сторону.
Шаг должен быть ненулевым.
И ещё: если вы идёте вниз (from: 10 к to: 0), шаг должен быть отрицательным, иначе вы вообще не будете приближаться к границе. В лучшем случае цикл даст пустой результат, в худшем — логика окажется сломанной, а вы будете долго искать “почему не печатает”.
Ошибка №5: ждать от stride(from:to:by:), что он включит правую границу.
stride(from:to:by:) работает как полуоткрытый интервал [start, end), то есть до end он “не доходит”.
Если вам нужно включать правую границу, вы либо используете through: (и проверяете достижимость), либо аккуратно подбираете to: как “на единицу дальше” для целых чисел.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ