> Что происходит при копировании и изменении массивов и вьюшек в Swift (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: nuum

Стек: iOS, Swift

> Пример ответа

Короткий ответ

В Swift массивы и другие коллекции из стандартной библиотеки реализуют copy-on-write (COW): при присваивании создаётся лёгкая копия, разделяющая storage, а реальное копирование происходит только при мутации. Вьюшки (например, ArraySlice, Substring) - это легковесные представления над тем же буфером, которые не владеют данными, но при изменении также запускают COW. Это даёт предсказуемое поведение: изменение одной переменной не влияет на другую, но с оговорками для классов внутри элементов.

Подробное объяснение

Массивы в Swift - это структуры с value semantics. При копировании var a = [1,2,3]; var b = a обе переменные ссылаются на один и тот же буфер в памяти. Пока никто не мутирует данные, это безопасно и эффективно. Как только вы изменяете b.append(4), Swift проверяет, уникальна ли ссылка на буфер (через isKnownUniquelyReferenced). Если нет - создаётся новая копия буфера, и только затем применяется мутация. Это и есть copy-on-write.

Вьюшки (ArraySlice, Substring, Data в некоторых контекстах) - это структуры, которые хранят ссылку на родительский буфер плюс диапазон индексов. Они не копируют элементы. Например, let slice = array[1..<3] - это вьюшка над array. Мутация вьюшки (если она объявлена как var) также триггерит COW: если вьюшка не единственный владелец буфера, создаётся новая копия именно для диапазона вьюшки.

Важный нюанс: COW работает на уровне буфера, а не элементов. Если массив содержит ссылочные типы (классы), копирование массива копирует только ссылки, а сами объекты остаются общими. Мутация массива (добавление/удаление) не затрагивает объекты, но изменение свойства объекта через любой из массивов видно во всех копиях.

Также стоит упомянуть: начиная с Swift 5, компилятор может оптимизировать некоторые копирования, но семантика остаётся той же. Для пользовательских типов COW не автоматический - его нужно реализовывать вручную через класс-обёртку и проверку уникальности.

На практике

На практике это означает:

  • При передаче массива в функцию и его мутации внутри - внешний массив не изменится, если функция не использует inout.
  • Срезы (ArraySlice) удобны для работы с подмассивами без копирования, но держат родительский буфер в памяти - это может привести к утечкам памяти, если срез маленький, а родительский массив огромный.
  • При работе с большими массивами и частыми мутациями стоит помнить, что COW может вызывать неожиданные копирования, если массив передаётся по нескольким путям.
  • Для производительности в циклах лучше использовать withUnsafeBufferPointer или ContiguousArray, если нужен гарантированный непрерывный буфер.

Пример кода

SWIFT
var array = [1, 2, 3]
var copy = array // COW: общий буфер, копирования нет
copy.append(4) // теперь буферы разделяются
print(array) // [1, 2, 3]
print(copy) // [1, 2, 3, 4]
let slice = array[1...] // ArraySlice, ссылается на буфер array
var mutableSlice = slice
mutableSlice[0] = 99 // COW для среза: копируется только диапазон
print(array) // [1, 2, 3] - не изменился
print(mutableSlice) // [99, 2, 3]
// Классы внутри массива
class Box { var value = 0 }
var boxes = [Box(), Box()]
var boxesCopy = boxes
boxesCopy[0].value = 5 // меняется объект, не массив
print(boxes[0].value) // 5 - общий объект

Как отвечать на собеседовании

Начните с чёткого определения value semantics и COW. Затем объясните механизм: общий буфер, проверка уникальности при мутации, ленивое копирование. Перейдите к вьюшкам: что это, как работают, когда полезны и какие подводные камни (удержание памяти). Обязательно упомяните разницу между копированием массива и копированием элементов-классов. Если спросят про производительность - говорите о сложности O(1) для копирования без мутации и O(n) для первой мутации. Можно добавить, как реализовать COW для своего типа через isKnownUniquelyReferenced.

Что проверяет интервьюер

Интервьюер оценивает:

  • Понимание value vs reference semantics.
  • Знание внутреннего устройства коллекций Swift, а не только поверхностного синтаксиса.
  • Умение объяснить trade-off между производительностью и безопасностью.
  • Осведомлённость о поведении вьюшек и их отличиях от массивов.
  • Способность предвидеть неочевидные последствия (например, удержание памяти срезом).

Типичные ошибки

  • Утверждение, что копирование массива всегда копирует элементы - это не так из-за COW.
  • Игнорирование того, что классы внутри массива остаются общими.
  • Путаница между ArraySlice и Array: срез не имеет собственного индекса с нуля.
  • Забывают, что COW не работает автоматически для пользовательских структур.
  • Считают, что вьюшки всегда безопасны - на самом деле они могут держать большой родительский буфер в памяти.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью