В Swift две переменные содержат один Array: что происходит с памятью при изменении одной из них?

В Swift две переменные содержат один Array: что происходит с памятью при изменении одной из них?

Проходите собеседования с ИИ помощником Hintsage

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

Array имеет семантику значения: после присваивания обе переменные логически независимы. Swift обычно не копирует буфер сразу, а использует copy-on-write; при изменении одной переменной, если буфер разделяется, создаётся отдельная копия, поэтому другая переменная не меняется.

Исторический контекст

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

Copy-on-write решает этот компромисс: неизменяемые копии могут временно использовать одно хранилище, а копирование откладывается до первой операции, которая способна изменить данные.

Постановка проблемы

Разработчик может ошибочно считать, что присваивание большого массива всегда немедленно удваивает его память, либо, наоборот, ожидать, что изменение одной переменной изменит и другую. Первое приводит к необоснованной оптимизации, второе — к ошибкам в состоянии приложения.

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

Подробное решение

При присваивании массива Swift передаёт дескриптор его хранилища, а не обязательно все элементы. Пока хранилище не изменяется, несколько значений могут разделять один буфер.

Перед мутацией реализация проверяет, является ли хранилище уникальным для изменяемого значения. Если нет, создаётся новый буфер, элементы копируются в него, и операция выполняется уже над новым хранилищем. Поэтому изменение одной переменной не нарушает значение другой.

Если хранилище уникально, его можно изменить на месте без копирования элементов. Это делает последовательные мутации эффективными, но не превращает Array в ссылочный тип: наблюдаемое поведение по-прежнему соответствует независимым значениям.

Минимальный пример:

var first = [1, 2, 3] var second = first first.append(4) print(first) // [1, 2, 3, 4] print(second) // [1, 2, 3]

До append массивы могут разделять буфер. Во время append при необходимости создаётся отдельное хранилище для first; second продолжает ссылаться на исходные элементы.

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

Ситуация из практики

В экранном состоянии хранят большой массив моделей. Для фильтрации создаётся рабочая переменная, после чего в ней удаляются элементы. Вариант с немедленным ручным копированием гарантирует независимость, но может без необходимости занять дополнительную память ещё до первой мутации.

Вариант полагаться на семантику Array и выполнить мутацию рабочей переменной обычно лучше: до изменения буфер может разделяться, а при первой записи копирование произойдёт автоматически. Если модели являются ссылочными объектами и их внутреннее состояние тоже нужно изолировать, одного copy-on-write массива недостаточно — потребуется копировать сами модели или использовать значения-структуры.

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

Что кандидаты часто упускают

  1. Всегда ли присваивание Array приводит к немедленному копированию всех элементов?

Нет. Семантически создаётся независимое значение, но физическое копирование обычно откладывается благодаря copy-on-write. Поэтому присваивание может быть дешёвым, пока разделяемое хранилище не потребуется изменить.

  1. Что произойдёт, если Array содержит классы?

Копируется структура массива и его ссылки на элементы, но не сами экземпляры классов. После copy-on-write два массива будут иметь разные буферы, однако соответствующие элементы могут ссылаться на один объект. Изменение свойства такого объекта будет видно через оба массива.

  1. Можно ли рассчитывать на отсутствие копирования при передаче Array в функцию?

Нельзя гарантировать это на уровне прикладного кода. Передача значения часто позволяет разделять хранилище, но последующая мутация, особенности оптимизации или необходимость сохранить данные могут привести к копированию. Правильный контракт — независимая семантика значения, а не обещание конкретного количества аллокаций.