JavaRush /Курсы /Swift SELF /Диапазоны ... и ..< и stride(from:to:by:)

Диапазоны ... и ..< и stride(from:to:by:)

Swift SELF
8 уровень , 1 лекция
Открыта

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 включительно” Да
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: как “на единицу дальше” для целых чисел.

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