Объединение массивов в Swift: от + до copy-on-write

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

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

Начать курс

Как объединять массивы в Swift? Полный разбор с примерами и секретами производительности

Если вы когда-нибудь писали на Swift, то наверняка сталкивались с задачей: «У меня есть два массива, как сделать из них один?». Вопрос кажется простым, но за ним скрывается целый мир оптимизаций и нюансов. На Stack Overflow этот вопрос набрал тысячи просмотров — и не зря. Сегодня мы не просто покажем, как писать код, а заглянем под капот компилятора и узнаем, что на самом деле происходит, когда вы пишете array1 + array2. Поехали!

Самый очевидный способ: оператор +

Давайте начнём с классики. В Swift массивы можно складывать как числа:

let first = [1, 2, 3]
let second = [4, 5, 6]
let result = first + second
print(result) // [1, 2, 3, 4, 5, 6]

Красиво? Да. Быстро? Не всегда. Когда вы используете +, Swift создаёт новый массив и копирует в него элементы из обоих исходных массивов. Если ваши массивы большие (скажем, по 100 000 элементов), это двойная работа для процессора. Но подождите — есть нюанс, который называется copy-on-write (COW).

Магия Copy-on-Write: как Swift экономит память

Swift — умный язык. Когда вы просто присваиваете один массив другому, он не копирует данные сразу:

var a = [1, 2, 3]
var b = a // Пока данные не изменяются, a и b ссылаются на одну область памяти
b.append(4) // Вот тут происходит реальное копирование: b получает свою копию

Это называется copy-on-write. Но при объединении через + это не работает — результат всегда новый массив. Почему? Потому что мы не просто ссылаемся на старые данные, а создаём новую коллекцию из элементов двух разных источников. Компилятор не может гарантировать, что исходные массивы не изменятся, поэтому он делает полную копию.

Метод append(contentsOf:): когда скорость важнее

Если вам нужно добавить элементы одного массива в конец другого, не создавая третий массив, используйте append(contentsOf:):

var numbers = [1, 2, 3]
let moreNumbers = [4, 5, 6]
numbers.append(contentsOf: moreNumbers)
print(numbers) // [1, 2, 3, 4, 5, 6]

В этом случае Swift не создаёт новый массив — он расширяет существующий. Если в массиве numbers достаточно зарезервированной памяти (capacity), то новые элементы просто копируются в свободное место. Если нет — происходит перераспределение памяти, но это всё равно быстрее, чем создавать массив с нуля.

Совет профи: Если вы знаете, что будете много добавлять элементы, заранее выделите память с помощью reserveCapacity():

var bigArray: [Int] = []
bigArray.reserveCapacity(1000)
for i in 0..<1000 {
    bigArray.append(i) // Ни одного лишнего копирования!
}

Метод flatMap: объединение массивов с трансформацией

Иногда нужно не просто соединить массивы, а ещё и преобразовать каждый элемент. Тут на помощь приходит flatMap (или compactMap для опционалов). Строго говоря, flatMap предназначен для «сплющивания» коллекций, но его можно использовать и для конкатенации:

let arrays = [[1, 2], [3, 4], [5, 6]]
let flattened = arrays.flatMap { $0 }
print(flattened) // [1, 2, 3, 4, 5, 6]

Под капотом flatMap делает то же самое, что и цикл с append(contentsOf:). Но код становится чище и читаемее. Правда, если вам нужно просто соединить два массива — не используйте flatMap, это избыточно.

reduce(into:) для гибкого объединения

Если у вас есть массив массивов и вы хотите объединить их с дополнительной логикой, попробуйте reduce(into:):

let arrayOfArrays = [[1, 2], [3, 4], [5, 6]]
let merged = arrayOfArrays.reduce(into: [Int]()) { result, next in
    result.append(contentsOf: next)
}
print(merged) // [1, 2, 3, 4, 5, 6]

Этот метод особенно полезен, когда нужно не просто склеить, а, например, отфильтровать или изменить элементы в процессе. Плюс он работает с inout параметром, что минимизирует копирование.

Заглядываем в исходники Swift: что происходит на уровне C++

Swift написан на C++, и структура Array — это обёртка над _ContiguousArrayStorage. Когда вы вызываете append(contentsOf:), происходит следующее:

  1. Проверяется, достаточно ли ёмкости (capacity) в текущем буфере.
  2. Если да — элементы копируются через memmove (быстрое копирование памяти).
  3. Если нет — выделяется новый буфер в 2 раза больше старого (стратегия геометрического роста), старые элементы копируются, потом добавляются новые.

Оператор + работает иначе: он сразу создаёт буфер размером a.count + b.count и копирует оба массива. Это оптимально, если вы всё равно собираетесь использовать результат как новый массив, но плохо, если вы могли бы обойтись без копирования.

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

Давайте посмотрим на бенчмарк (условный, для понимания):

  • Оператор +: ~0.05 сек для 100 000 элементов (два массива по 50 000).
  • append(contentsOf:): ~0.03 сек для того же объёма (если capacity уже выделена).
  • flatMap: ~0.04 сек (дополнительная нагрузка на замыкание).
  • reduce(into:): ~0.04-0.05 сек (зависит от сложности замыкания).

Разница не гигантская, но если вы работаете с большими данными или в цикле — каждый микросекунда на счету. Для 80% случаев используйте append(contentsOf:) — это золотая середина между скоростью и читаемостью.

Частые ошибки и подводные камни

Вот несколько моментов, которые сбивают с толку даже опытных разработчиков:

  • Изменение исходного массива после объединения: если вы сделали let c = a + b, то c — независимая копия. Меняйте a и b сколько угодно

Блоги

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