JavaRush /Курси /Swift SELF /Індекси, count, isEmpty та помилки out-of-range

Індекси, count, isEmpty та помилки out-of-range

Swift SELF
Рівень 11 , Лекція 2
Відкрита

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])  // Какао

Щоб закріпити це візуально, ось коротка таблиця:

Індекс Елемент
0
"Кава"
1
"Чай"
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 є багато корисних властивостей і методів. Ми поки що не будемо вивчати їх усі детально, але корисно один раз побачити «карту місцевості», щоб розуміти: масив — це не просто квадратні дужки та індекси, у нього є свої інструменти.

Властивість / метод Що робить Приклад
array.count
Кількість елементів
tasks.count
array.isEmpty
Перевіряє, чи масив порожній
tasks.isEmpty
array.first
Перший елемент або nil, якщо масив порожній
tasks.first
array.last
Останній елемент або nil, якщо масив порожній
tasks.last
array.append(x)
Додає елемент у кінець
tasks.append("Нове завдання")
array.insert(x, at:)
Вставляє елемент за індексом
tasks.insert("Терміново", at: 0)
array.remove(at:)
Видаляє елемент за індексом
tasks.remove(at: 1)
array.removeLast()
Видаляє останній елемент
tasks.removeLast()
array.contains(x)
Перевіряє, чи є елемент у масиві
tasks.contains("Поспати")

Наприклад:

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.

1
Задача
Swift SELF, 11 рівень, 2 лекція
Недоступна
Останній елемент
Останній елемент
1
Задача
Swift SELF, 11 рівень, 2 лекція
Недоступна
Індекс без ризику
Індекс без ризику
1
Задача
Swift SELF, 11 рівень, 2 лекція
Недоступна
Безпечний доступ
Безпечний доступ
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ