1. Індекси та межі масиву
Коли ви вперше дивитеся на масив, він здається милим і безневинним: «ну список і список». Але потім ви звертаєтеся до array[0], потім до array[1], а десь у темряві вже чекає array[array.count]… і програма драматично падає. Це нормально: ви тут не перший і не останній.
У цій лекції ми не будемо героїчно «запам’ятовувати магічні правила». Натомість розберемося, які індекси допустимі, чому count — це не індекс, навіщо потрібен isEmpty і як писати код так, щоб масив не перетворював ваш запуск на шоу «Fatal error».
Нульова індексація: чому перший елемент — це [0]
Ось вам невеликий жарт всесвіту: майже в усіх популярних мовах програмування перший елемент масиву має індекс 0. Чому так історично склалося — окрема філософська тема. На практиці це просто правило гри.
Візьмемо масив із трьох елементів:
let drinks = ["Кава", "Чай", "Какао"]
print(drinks[0]) // Кава
print(drinks[1]) // Чай
print(drinks[2]) // Какао
Щоб закріпити це візуально, ось коротка таблиця:
| Індекс | Елемент |
|---|---|
|
|
|
|
|
|
Думка дня: Якщо в масиві count елементів, допустимі індекси — від 0 до count - 1.
Тобто для count = 3 допустимі індекси: 0, 1, 2.
Діапазон допустимих індексів: 0..<array.count
Після циклів ви вже бачили діапазони 0..<n і 1...n. Тепер вони повертаються, але вже з дуже практичним змістом: діапазон допустимих індексів масиву — це 0..<array.count.
Чому саме так? Тому що ..< означає «до, але не включно з правою межею». Якщо count = 3, то 0..<3 дає індекси 0, 1, 2 — саме те, що нам потрібно.
Ось як зазвичай виглядає така перевірка:
let drinks = ["Кава", "Чай", "Какао"]
let index = 2
if (0..<drinks.count).contains(index) {
print(drinks[index]) // Какао
}
А ось тут новачки часто помиляються: вони пишуть 0...drinks.count. Якщо count = 3, то 0...3 включає 3. А індекс 3 — це вже позиція після останнього елемента, а не сам елемент.
Можна запам’ятати так: count — це «скільки коробок», а індекс count — це «місце після коробок», але не коробка.
out-of-range: що це і чому Swift падає
Помилки виходу за межі масиву називають по-різному: out-of-range, out of bounds, «виліт за індексом», «чому воно впало, я ж просто подивився елемент». Суть одна: ви намагаєтеся звернутися до неіснуючого елемента.
У Swift звичайний доступ за індексом array[index] — це контракт: «індекс має бути коректним». Якщо контракт порушено, програма завершується (відбувається trap). Це зроблено навмисно: некоректний індекс — це логічна помилка в програмі, і Swift воліє впасти одразу і голосно, ніж продовжити роботу з неправильними даними. У дизайні колекцій Swift це сформульовано прямо: доступ за індексом не повертає Optional, бо коректність індексу вважається обов’язком коду, а порушення — irrecoverable logic error.
Приклад «як зіпсувати собі вечір»:
let drinks = ["Кава", "Чай", "Какао"]
print(drinks[3]) // Fatal error: Index out of range
І тут важливо зрозуміти: це не «примха Swift». Це захист. Якби мова мовчала, ви б отримали сміттєві дані, а потім 40 хвилин шукали б, чому у вас «Чай» раптом став «Кава», а знижка стала від’ємною. Падіння — неприємно, але чесно.
count — не індекс: часта пастка
Типова ситуація для новачка: ви хочете отримати останній елемент і пишете так:
let drinks = ["Кава", "Чай", "Какао"]
let last = drinks[drinks.count] // помилка
print(last)
Чому це помилка? Тому що drinks.count == 3, а останній індекс дорівнює 2.
Правильний варіант — із обов’язковою перевіркою, чи масив не порожній:
let drinks = ["Кава", "Чай", "Какао"]
if !drinks.isEmpty {
let last = drinks[drinks.count - 1]
print(last) // Какао
}
Зверніть увагу на стиль: ми не обчислюємо count - 1, поки не переконаємося, що масив не порожній. Це не формальність — це звичка писати код, який не вибухає від порожнього введення.
Схема: де розташовані індекси і чому count — це «після останнього»
Зараз закріпімо інтуїцію, бо на ній тримається вся подальша розмова про масиви.
Уявіть масив із 4 елементів. Тоді:
- індекси елементів: 0 1 2 3
- count == 4
- позиція 4 — це «після останнього елемента»
У вигляді схеми:
індекси: 0 1 2 3
елементи: [A] [B] [C] [D]
^ ^
| |
початок (0) останній (count - 1)
позиція після масиву: count (4) → елемента немає
І звідси майже автоматично випливає правильний діапазон: 0..<count.
2. count і isEmpty
Коли у вас є масив, ви майже завжди хочете знати одну з двох речей: «скільки елементів?» і «він узагалі порожній?». У Swift це оформлено максимально прямо: властивість count відповідає за кількість елементів, а isEmpty — за перевірку порожнечі.
Важливо звикнути до думки: count — це кількість елементів, а не «останній індекс». Останній індекс — це count - 1, але тільки якщо масив не порожній. Інакше count - 1 перетворюється на -1, а індекси так не працюють.
Подивімося на короткий приклад:
let tasks = ["Помити чашку", "Зробити ДЗ", "Поспати"]
print(tasks.count) // 3
print(tasks.isEmpty) // false
Тут усе спокійно: три завдання — отже, масив не порожній.
А тепер порожній масив:
let tasks: [String] = []
print(tasks.count) // 0
print(tasks.isEmpty) // true
Якщо масив порожній, count == 0, а isEmpty == true. На перший погляд — майже те саме. Але isEmpty зазвичай читається краще: мозку простіше сприйняти «масив порожній», ніж «кількість дорівнює нулю». І так, ми пишемо код не лише для компілятора, а й для людей — зокрема для вас у майбутньому, які відкриють проєкт через тиждень і спитають: «Хто це писав?».
isEmpty як «стоп-сигнал» перед першим елементом
Є особливий клас помилок: спроба взяти array[0], коли масив порожній. Логічно це окремий випадок out-of-range, але за частотою — майже окремий жанр.
Тому корисна звичка проста: якщо ви хочете взяти перший або останній елемент, спочатку перевірте, чи масив не порожній.
let tasks: [String] = []
if tasks.isEmpty {
print("Список завдань порожній")
} else {
print("Перше завдання: \(tasks[0])")
}
Цей код читається як нормальний людський текст. І це добре: чим менше припущень у коді, тим менше сюрпризів.
Популярні властивості та методи масиву
У масиву в Swift є багато корисних властивостей і методів. Ми поки що не будемо вивчати їх усі детально, але корисно один раз побачити «карту місцевості», щоб розуміти: масив — це не просто квадратні дужки та індекси, у нього є свої інструменти.
| Властивість / метод | Що робить | Приклад |
|---|---|---|
|
Кількість елементів | |
|
Перевіряє, чи масив порожній | |
|
Перший елемент або nil, якщо масив порожній | |
|
Останній елемент або nil, якщо масив порожній | |
|
Додає елемент у кінець | |
|
Вставляє елемент за індексом | |
|
Видаляє елемент за індексом | |
|
Видаляє останній елемент | |
|
Перевіряє, чи є елемент у масиві | |
Наприклад:
let tasks = ["Помити чашку", "Зробити ДЗ", "Поспати"]
print(tasks.count) // 3
print(tasks.isEmpty) // false
print(tasks.first) // Optional("Помити чашку")
print(tasks.last) // Optional("Поспати")
Поки що для нас найважливіші властивості — це count і isEmpty, тому що саме вони допомагають безпечно працювати з індексами. Але вже зараз корисно знати, що в масиву є й інші зручні інструменти, до яких ми незабаром повернемося.
3. Міні-застосунок TaskBox
Тепер зберемо невеликий фрагмент консольного застосунку, який далі розвиватиметься в лекціях про перебір і мутації. Сьогодні наша мета скромна: навчитися безпечно діставати завдання за номером.
Уявімо, що користувач вводить номер завдання так, як це роблять люди, — з 1, а ми хочемо показати відповідний елемент масиву, який нумерується з 0. Це дуже життєва ситуація: людям зручніше рахувати від 1, а масив — від 0.
Перетворення номера користувача на індекс масиву
Найпростіша логіка така: якщо користувач вводить 1, то індекс має бути 0. Отже:
index = numberFromUser - 1
Але спочатку нам потрібно безпечно прочитати число.
let tasks = ["Помити чашку", "Зробити ДЗ", "Поспати"]
let line = readLine() ?? ""
let number = Int(line) ?? 0
let index = number - 1
print("Ви ввели номер: \(number), індекс: \(index)")
Якщо користувач введе нісенітницю, Int(line) дасть nil, і ми підставимо 0. Це не ідеальний користувацький досвід, але зараз наше завдання — контроль над типами та безпекою. Поліпшувати повідомлення будемо пізніше.
Перевірка меж перед доступом
Тепер головне: перш ніж звертатися до tasks[index], ми зобов’язані перевірити, що індекс потрапляє в допустимий діапазон.
let tasks = ["Помити чашку", "Зробити ДЗ", "Поспати"]
let number = Int(readLine() ?? "") ?? 0
let index = number - 1
if index >= 0 && index < tasks.count {
print("Завдання: \(tasks[index])")
} else {
print("Немає завдання з номером \(number)")
}
Це і є базовий захист від out-of-range. Так, умова виглядає трохи громіздко. Але вона чесна й зрозуміла: індекс не має бути від’ємним і має бути меншим за count.
4. Безпечний доступ і Optional
До цього моменту ми розв’язували проблему так: «якщо індекс поганий — виводимо повідомлення». Це нормально. Але часто зручніше зробити окрему функцію, яка повертає Optional, тобто акуратно моделює ситуацію «елемента може не бути».
І тут ми використовуємо те, що раніше проходили: Optional і if let.
func safeTask(at index: Int, in tasks: [String]) -> String? {
if index >= 0 && index < tasks.count {
return tasks[index]
}
return nil
}
let tasks = ["Помити чашку", "Зробити ДЗ", "Поспати"]
let index = 10
if let task = safeTask(at: index, in: tasks) {
print("Завдання: \(task)")
} else {
print("За індексом \(index) завдання немає")
}
Сенс цієї конструкції дуже типовий для Swift: ми не вдаємо, що елемент завжди існує. Ми прямо кажемо: функція може повернути рядок, а може повернути nil. І код, що її викликає, зобов’язаний це обробити.
Чому array[index] не повертає Optional
Іноді виникає запитання: «А чому не можна зробити так, щоб array[index] повертав Element?, і тоді не буде падінь?» У Swift це обговорювали багато разів, але нинішню поведінку зроблено навмисно: звичайний сабскрипт масиву — швидкий і строгий, а неправильний індекс вважається логічною помилкою, а не «звичайною ситуацією».
Водночас Swift усе одно дає вам інструменти для «звичайних ситуацій»: first, last, randomElement() та подібні методи зазвичай повертають Optional, тому що там відсутність значення — очікуваний сценарій (наприклад, «першого елемента немає, бо масив порожній»). Ідея така: коли відсутність елемента — це нормально, використовуємо Optional в API; коли індекс неправильний — це баг, і краще впасти одразу.
У межах нашого курсу це перетворюється на просте правило: якщо ви самі обчислюєте індекс, значить ви відповідаєте за його коректність. Якщо ви не впевнені — зробіть перевірку або обгорніть доступ у функцію, що повертає Optional.
5. Типові помилки під час роботи з індексами, count та isEmpty
Помилка №1: вважати, що count — це останній індекс.
Це, мабуть, найпопулярніша пастка. count — це кількість елементів, а не «максимальний індекс». Якщо елементів 3, то count == 3, а останній індекс — 2. Звичка «останній індекс = count - 1, але тільки якщо масив не порожній» рятує від багатьох падінь.
Помилка №2: використовувати 0...array.count для перебору індексів.
На вигляд це здається логічним: «від нуля до кількості». Але це включний діапазон, і він обов’язково захопить array.count, а це вже out-of-range. Для індексів майже завжди потрібен 0..<array.count. Якщо рука автоматично тягнеться до ..., зупиніться й перевірте: права межа має включатися чи ні.
Помилка №3: звертатися до array[0] без перевірки порожнечі.
Іноді здається: «Ну масив же має бути не порожній». А потім введення користувача, тестові дані або випадковий сценарій роблять його порожнім, і програма падає. isEmpty — це дешева й дуже читабельна страховка. Краще один раз написати перевірку, ніж потім пояснювати собі, чому застосунок падає «іноді».
Помилка №4: забувати про від’ємні індекси.
Коли ви перетворюєте номер користувача на індекс (number - 1), легко отримати -1. У Swift -1 — не «особливий індекс», це просто неправильний індекс. Тому перевірка index >= 0 — обов’язкова частина безпечного доступу, особливо коли індекс обчислюється.
Помилка №5: намагатися «лагодити» проблему out-of-range через ! і надію.
Іноді хочеться зробити щось на кшталт «ну я знаю, що там точно є елемент», і почати форсувати розпакування та ігнорувати перевірки. Це працює рівно до першого несподіваного сценарію. А потім ви знову зустрічаєтеся з Fatal error — тільки вже в більш заплутаному місці. Набагато здоровіше або перевіряти межі, або повертати Optional і обробляти його через if let.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ