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]
^ ^
| |
start (0) last (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. Это не идеальный UX, но сейчас наша задача — контроль над типами и безопасностью. Улучшать сообщения будем позже.
Проверка границ перед доступом
Теперь главное: прежде чем делать 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) задачи нет")
}
Смысл этой конструкции очень «свифтовый»: мы не делаем вид, что элемент всегда существует. Мы говорим честно: функция может вернуть строку, а может вернуть 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ