Как объединять массивы в 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:), происходит следующее:
- Проверяется, достаточно ли ёмкости (
capacity) в текущем буфере. - Если да — элементы копируются через
memmove(быстрое копирование памяти). - Если нет — выделяется новый буфер в 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сколько угодно