2. Массив классов

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

1class Entity { 2 var id: String 3 init(id: String) { self.id = id } 4} 5 6class EntityContainer { 7 var entities: [Entity] = [Entity(id: "a"), 8 Entity(id: "b"), 9 Entity(id: "c")] 10} 11 12let container = EntityContainer() 13var entities = container.entities 14 15entities[0].id = "d" 16var entity = entities.removeLast() 17entity.id = "e" 18 19print(container.entities) 20print(entities)

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

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

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

Темы

Компании

Ответ

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

1class Entity { ... } 2 3class EntityContainer { 4 var entities: [Entity] = ... 5}

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

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

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

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

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

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

1entities[0].id = "d"

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

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

ID2_3

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

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

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

ID2_4

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

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

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

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

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

1class Entity { 2 var id: String 3 init(id: String) { self.id = id } 4 5 func copy() -> Entity { 6 return Entity(id: self.id) 7 } 8} 9 10let container = EntityContainer() 11var entities = container.entities.map { $0.copy() }