3. Массив классовКод

Нужно понять, что будет выведено, и объяснить, почему.

class Entity { var id: String init(id: String) { self.id = id } } class EntityContainer { var entities: [Entity] = [Entity(id: "a"), Entity(id: "b"), Entity(id: "c")] } let container = EntityContainer() var entities = container.entities entities[0].id = "d" var entity = entities.removeLast() entity.id = "e" print(container.entities) print(entities)

Дополнительные вопросы

1. Почему значения меняются в обоих массивах? Array это value type, но после присваивания меняются элементы и в копии, и в оригинале.

2. Как изменить код так, чтобы изменение в одном массиве не затрагивало другой? Один способ: сделать Entity структурой, чтобы у элементов была семантика value type. Есть ли другие способы?

Темы

Компании

Ответ

Сразу обращаем внимание на то, что свойство entities класса EntityContainer это массив классов Entity.

class Entity { ... } class EntityContainer { var entities: [Entity] = ... }

Причем для нас важно именно то, что это массив. В Swift Array это value type. А это значит, что во время его присваивания происходит копирование исходного массива.

let container = EntityContainer() var entities = container.entities // entities - независимая копия container.entities

Но и тут скрывается важный момент. Для стандартных типов коллекций (Array, Dictionary, Set) Swift использует оптимизацию под названием Copy-on-Write.

Это значит, что при присваивании копирования массива не происходит по умолчанию. Обе переменные пока используют одни и те же элементы. Копирование же происходит в момент изменения одной из переменных, т.е. именно тут:

var entities = container.entities // Оптимизация COW, копирование массива еще не происходит entities[0].id = "d" // При изменении происходит копирование массива. // Теперь entities и container.entities это две независимые копии.
ID2_1
var entities = container.entities
ID2_2
entities[0].id = "d"

Теперь рассмотрим подробнее изменение элемента массива:

entities[0].id = "d"

Да, массив ранее скопировался. Но давайте посмотрим, что в массиве. А в массиве класс Entity, который является reference type.

Это значит, что даже после копирования массива, его элементы ссылаются на одни и те же области памяти. Поэтому, когда мы поменяем id первого элемента массива entities, то мы поменяем и значение в массиве container.entities.

ID2_3

Далее рассмотрим строчки с удалением элемента из массива:

var entity = entities.removeLast() // Удаляем последний элемент из массива entities entity.id = "e" // Удаленный элемент все еще ссылается на ту же область памяти, что и последний элемент из container.entities

Метод массива removeLast() удаляет последний элемент и возвращает его. Этот удаленный элемент будет типа Entity, т.е. reference type. А мы не забываем, что в исходном массиве container.entities последний элемент все так же ссылается на эту же область памяти. Соответственно, изменения id удаленного элемента отразятся на исходном массиве.

ID2_4

Что и приводит нас к следующему ответу:

print(container.entities) // d, b, e print(entities) // d, b

Дополнительные вопросы

1. Потому что элементы внутри массива это классы, а значит reference type. Даже после копирования массива, новые элементы являются ссылками на те же области памяти.

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

class Entity { var id: String init(id: String) { self.id = id } func copy() -> Entity { return Entity(id: self.id) } } let container = EntityContainer() var entities = container.entities.map { $0.copy() }

entities получает копию массива, но не копию объектов. Array это value type, а Entity класс: в буфере лежат ссылки. Запись id меняет сам объект, поэтому её видно и из container.entities, и из entities. removeLast меняет уже массив. Тут буфер ссылок копируется, и из entities пропадает последний элемент. Ссылка на него остаётся в entity и в container. Поэтому entity.id = "e" меняет объект, который по-прежнему лежит в контейнере.

// после entities[0].id = "d" // оба массива смотрят на объекты d, b, c // после removeLast и entity.id = "e" // container.entities: d, b, e // entities: d, b

Дополнительные вопросы

1. Меняются оба массива, потому что копируются ссылки, а не экземпляры Entity. Пока буфер общий, это одни и те же объекты. После копирования буфера ссылки внутри него всё равно указывают на те же экземпляры. Правка id не создаёт новый объект.

2. Сделать Entity структурой: тогда элемент копируется вместе с массивом. Если класс нужен, при отделении копии создавать новые экземпляры, например entities.map { Entity(id: $0.id) }. Оба способа убирают общую ссылку.