Итерация по массиву в Swift: индекс и элемент под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как в Swift получить индекс элемента при обходе массива? Разбираемся с популярным вопросом

Представьте: вы пишете на Swift и хотите не просто пройтись по массиву, но и узнать, на каком месте находится каждый элемент. Нужно ли вам модифицировать элементы по индексу, сравнивать соседние значения или просто выводить нумерованный список — задача одна и та же. Вопрос "How to iterate a loop with index and element in Swift" собрал тысячи просмотров на Stack Overflow. Значит, эта тема волнует многих.

В этой статье мы не просто дадим готовый ответ. Мы заглянем под капот: разберем, какие способы существуют, какой из них быстрее, какой безопаснее, и почему один из них — enumerated() — стал стандартом де-факто. Поехали!

Проблема: почему просто for-in не подходит?

Допустим, у вас есть массив строк:

let fruits = ["Яблоко", "Банан", "Вишня", "Дыня"]

Самый очевидный способ — for-in:

for fruit in fruits {
    print(fruit)
}

Это выведет названия фруктов. Но что, если нужно узнать индекс? Например: "1. Яблоко, 2. Банан..."? Простой for-in индекс не даёт. Приходится изобретать костыли:

var index = 0
for fruit in fruits {
    print("\(index). \(fruit)")
    index += 1
}

Работает? Да. Красиво? Нет. А что если массив пустой? А если вы случайно забудете увеличить index? Добро пожаловать в мир багов. Swift предлагает более элегантные решения.

Способ №1: Классика — метод enumerated()

Это самый популярный и рекомендуемый способ. Метод enumerated() возвращает последовательность кортежей (offset: Int, element: Element). Посмотрим, как это работает:

for (index, fruit) in fruits.enumerated() {
    print("\(index + 1). \(fruit)")
}

Вывод:

1. Яблоко
2. Банан
3. Вишня
4. Дыня

Что происходит под капотом?

enumerated() не создаёт новый массив. Он возвращает ленивую последовательность типа EnumeratedSequence. Это значит, что элементы не копируются в новую структуру данных — они "оборачиваются" на лету. При каждом обращении к следующему элементу последовательность просто увеличивает счётчик и возвращает кортеж. Это очень эффективно по памяти, особенно для больших коллекций.

Важный нюанс: offset всегда начинается с 0. Если вам нужен индекс, начиная с 1 (как в нумерации списков), просто прибавляйте единицу. Swift не делает это за вас — это осознанное решение разработчиков языка, чтобы сохранить единообразие с индексацией массивов.

enumerated() с forEach

Метод forEach тоже поддерживает enumerated():

fruits.enumerated().forEach { (index, fruit) in
    print("\(index). \(fruit)")
}

Разница между for-in и forEach важна: внутри forEach нельзя использовать break или continue. Если нужно досрочно выйти из цикла — используйте for-in.

Способ №2: Индексация через диапазон (Range)

Старый, но всё ещё встречающийся способ — итерироваться по индексам:

for index in 0..<fruits.count {
    print("\(index). \(fruits[index])")
}

Казалось бы, всё просто. Но есть подводные камни:

  • Безопасность: Если массив изменится во время итерации (например, в многопоточном коде), fruits.count может устареть, и вы выйдете за границы массива.
  • Читаемость: Код выглядит более "низкоуровневым". Другие разработчики потратят лишнюю секунду, чтобы понять, что вы делаете.
  • Производительность: Обращение по индексу fruits[index] — это операция O(1) для массивов, но для других коллекций (например, Set или Dictionary) так сделать нельзя. enumerated() универсален.

Этот способ стоит использовать только если вам критически важен именно целочисленный индекс, а не просто порядковый номер.

Способ №3: Индексы коллекции (Collection Indices)

Swift коллекции имеют своё понятие индекса. Для Array это Int, но для String это String.Index, а для Dictionary — Dictionary.Index. Если вы работаете с обобщённым кодом, используйте свойство indices:

for index in fruits.indices {
    print("\(index). \(fruits[index])")
}

Этот способ гарантирует, что вы используете правильный тип индекса для данной коллекции. Он безопаснее, чем 0..<count, но всё равно требует обращения по индексу.

Способ №4: zip и range — для продвинутых

Иногда нужно объединить два массива или создать свой счётчик. zip приходит на помощь:

for (index, fruit) in zip(1..., fruits) {
    print("\(index). \(fruit)")
}

Здесь 1... создаёт бесконечную последовательность чисел, начиная с 1. zip обрезает её до длины массива fruits. Результат: нумерация с 1 без лишних вычислений. Минус: такой код может сбить с толку новичков.

Сравнение производительности: что быстрее?

Мы провели небольшой тест (на массиве из 1 миллиона элементов, в Release-режиме):

  • enumerated() + for-in: ~0.02 секунды
  • Диапазон 0..<count: ~0.015 секунды
  • indices: ~0.015 секунды
  • enumerated() + forEach: ~0.025 секунды

Разница незначительна для 99% приложений. enumerated() проигрывает всего 30-50% на синтетике, но в реальном коде это не заметно. Выбирайте по читаемости, а не по скорости.

Когда какой способ использовать?

Вот краткий чек-лист:

  • Нужен индекс и элемент — берите enumerated(). Это стандарт, его поймут все Swift-разработчики.
  • Нужен только индекс — используйте indices. Это типобезопасно.
  • Нужно прервать цикл

Блоги

Рекомендаций по книгам