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) може з’являтися накопичення похибок через особливості арифметики з плаваючою крапкою: ви можете очікувати 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: так 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 лекція
Недоступна
Ракетний відлік
Ракетний відлік
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ