При копировании Array, содержащего экземпляры классов, что происходит с самими объектами до изменения одной...

При копировании Array, содержащего экземпляры классов, что происходит с самими объектами до изменения одной из копий?

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

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

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

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

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

Copy-on-write решает эту проблему: копии разделяют неизменяемое хранилище, а отдельный буфер создаётся только перед изменением одной из копий. ARC при этом управляет временем жизни ссылочных объектов, находящихся внутри хранилища.

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

Важно не перепутать копирование массива с копированием его элементов. Если массив содержит экземпляры классов, копируется значение-ссылка на каждый экземпляр, а не состояние самих объектов.

Неверное предположение о глубокой копии может привести к неожиданным изменениям: изменение свойства объекта через одну копию массива будет видно через другую. Неверное предположение о немедленном освобождении также опасно: удаление элемента из одной копии не уничтожает объект, если он остался в другой.

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

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

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

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

final class Item { var value: Int init(_ value: Int) { self.value = value } } var first = [Item(1)] var second = first first[0].value = 2 print(second[0].value) // 2 first.removeAll() print(second[0].value) // 2

В примере изменение свойства затрагивает общий объект. После removeAll() первая коллекция больше не владеет этим объектом, но second продолжает содержать сильную ссылку, поэтому объект не уничтожается.

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

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

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

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

  1. Увеличивает ли копирование массива счётчик сильных ссылок каждого элемента?

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

  2. Что произойдёт, если изменить свойство элемента после копирования массива?

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

  3. Когда экземпляр элемента может быть уничтожен после удаления его из одной копии?

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