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]
          ^                      ^
          |                      |
      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 есть много полезных свойств и методов. Мы пока не будем изучать их все подробно, но полезно один раз увидеть «карту местности», чтобы понимать: массив — это не просто квадратные скобки и индексы, у него есть свои инструменты.

Свойство / метод Что делает Пример
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. Это не идеальный 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.

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