Рассмотрим еще один вопрос, который спрашивают на iOS-собеседовании: есть ли разница при копировании массива и копировании структуры? Если есть, то какая? Данный вопрос тесно связан с механизмом Copy-on-Write. Посмотрим как он работает и разберем разницу между двумя типами копирования: поверхностным и глубоким копированием.
Содержание
Что такое механизм Copy-on-Write в Swift?
Copy-on-Write — это специальный механизм, позволяющий повысить производительность при копировании коллекций. Данный механизм работает по умолчанию только с массивами и словарями. Он не будет работать просто так в ваших собственных типах данных.
Как работает Copy-on-Write?
Представьте себе большой массив, если вы скопируете этот массив в другую переменную, то Swift нужно будет скопировать все элементы этого массива в новую переменную. А если объектов в массиве много, то это займет какое-то время. Чтобы оптимизировать такой процесс копирования, придумали следующее: когда вы указываете две переменные на один и тот же массив, то эти переменные будут указывать на одну и ту же область в памяти, в той, где и хранится массив. Как только вы попытаетесь изменить какую-либо переменную, то Swift создаст полную копию данных массива для этой переменной и после этого применит ваши изменения. Таким образом, две переменные в дальнейшем будут смотреть на разные адреса в области памяти.
Давайте посмотрим на пример:
import Foundation
var arrayA = [1, 2, 3]
let arrayB = arrayA
// Получить адрес массива
func address(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
address(arrayA) // Optional(0x600000fc01a0)
address(arrayB) // Optional(0x600000fc01a0)
arrayA.append(4)
address(arrayA) // Optional(0x6000039db2c0)
address(arrayB) // Optional(0x600000fc01a0)
Как видим, до того момента, пока мы не изменили первый массив (добавив новый элемент), два массива ссылались на одну область в памяти. После изменения — на разные. В этом и заключается оптимизация Copy-on-Write в коллекциях Swift.
Копирование массива с объектом типа class
Прежде чем мы посмотрим на пример, давайте поговорим про типы копирования. Существует два типа копирования: поверхностное и глубокое копирование.
- Поверхностное (или мелкое) — создается новая коллекция, которая хранит ссылки на объекты внутри этой коллекции. Если привести пример, то представьте что у вас есть массив с типом class. В этом массиве несколько объектов. При поверхностном копировании создастся новый массив, но объекты в нем не будут созданы заново, они будут просто скопированы по ссылке (то есть объекты будут указывать на одну и ту же область памяти, что и объекты исходного массива).
- Глубокое — создается новая коллекция, которая хранит копии на объекты внутри этой коллекции. То есть объекты внутри коллекции будут указывать на разные области в памяти по сравнению с исходным массивом.
Массивы могут поддерживать глубокое копирование только тогда, когда объекты массива поддерживают протокол NSCopying.
NSCopying declares one method, copy(with:), but copying is commonly invoked with the convenience method copy(). The copy() method is defined for all objects inheriting from NSObject and simply invokes copy(with:) with the default zone.
developer.apple.com
Давайте разберемся на примерах как работает поверхностное и глубокое копирование с учетом механизма Copy-on-Write.
Поверхностное копирование
import Foundation
// 1
class Contact: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
let copiedObj = Contact(name: name)
return copiedObj
}
var name = "none"
init(name: String) {
self.name = name
}
}
// 2
extension Array where Element: NSCopying {
public var copy: [Element] {
return self.map {$0.copy(with: nil) as! Element}
}
}
// 3
func address(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
// 4
var arrayA = [Contact]()
arrayA.append(Contact(name: "Ivan"))
// 5
var arrayB = arrayA // Поверхностное копирование
//var arrayB = arrayA.copy // Глубокое копирование
// 6
address(arrayA) // Optional(0x600003c8cd70)
address(arrayB) // Optional(0x600003c8cd70)
// 7
arrayB[0].name = "Petr"
// 8
print(arrayA[0].name) // Petr
print(arrayB[0].name) // Petr
// 9
address(arrayA) // Optional(0x600003c8cd70)
address(arrayB) // Optional(0x600003c8cd70)
- Создаем класс Contact, поддерживающий протокол NSCopying (для возможности глубокого копирования). Также, для реализации глубокого копирования нам необходимо реализовать метод copy(with: ).
- Для того чтобы наши массивы могли поддерживать глубокое копирование, нам необходимо реализовать расширение для массива, где с помощью функции map{} производим копирование каждого экземпляра. copy() — возвращает объект, возвращенный функцией copy(with: ).
- С функцией address() мы уже знакомы. Эта функция возвращает текстовое представления адреса в памяти, передаваемого в нее объекта.
- Далее создаем массив A и добавляем в него единственный объект типа Contact с именем Ivan.
- Сначала разберемся с поверхностным копированием. Создаем массив B присвоением массива A
- Сработал механизм Copy-on-Write, поэтому адреса этих массивов в памяти будут одинаковы.
- Изменяем единственный объект массива B. Меняем имя на Petr.
- Как мы видим. Оба массива распечатали одинаковое значение — Petr.
- Так же, адреса в памяти двух массивов остались одинаковыми.
Почему мы получили такой результат при печати объектов массивов? И почему адреса в памяти двух массивов не изменились?
В тот момент, когда мы произвели поверхностное копирование создался массив B и так как в силу вступает механизм Copy-on-Write, то адреса этих массивов в памяти будут одинаковы. Так как поверхностное копирование не создает новые объекты внутри массива, а только хранит ссылки на объекты внутри него, то массивы A и B хранят одинаковую ссылку на объект Contact(name: «Ivan»).
Поэтому, после того, как вы изменили объект в одном массиве по ссылке, его значение изменилось и в другом. И так как вы по факту не изменили объект одного из массива, то Copy-on-Write по-прежнему работает для этих двух массивов и в конце программы выдает одинаковые адреса в памяти.
Глубокое копирование
Используя предыдущий пример, переключим тип копирования и посмотрим что получилось:
import Foundation
class Contact: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
let copiedObj = Contact(name: name)
return copiedObj
}
var name = "none"
init(name: String) {
self.name = name
}
}
extension Array where Element: NSCopying {
public var copy: [Element] {
return self.map {$0.copy(with: nil) as! Element}
}
}
func address(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
var arrayA = [Contact]()
arrayA.append(Contact(name: "Ivan"))
// 1
//var arrayB = arrayA // Поверхностное копирование
var arrayB = arrayA.copy // Глубокое копирование
// 2
address(arrayA) // Optional(0x000060000360f830)
address(arrayB) // Optional(0x0000600000735190)
arrayB[0].name = "Petr"
// 3
print(arrayA[0].name) // Ivan
print(arrayB[0].name) // Petr
address(arrayA) // Optional(0x000060000360f830)
address(arrayB) // Optional(0x0000600000735190)
- Переключаем тип копирования на глубокое копирование.
- Можно заметить, что адреса массивов смотрят на разные области в памяти.
- При выводе объектов массивов получаем разные значения.
Так как при глубоком копировании мы не храним ссылки, а заново создаем объекты массива (по факту добавляем во второй массив новые объекты), то механизм Copy-on-Write не применяется к нашим массивам. Как мы видим, адреса этих массивов указывают в разные области памяти, как и объекты внутри массивов. Поэтому при изменении объекта в массиве B мы не затрагиваем объект из массива A.
Копирование структур
Так как механизм Copy-on-Write работает по умолчанию для массивов и словарей, то при копировании структур создается отдельное место в памяти. Посмотрим простой пример:
import Foundation
func address(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
struct Contact {
var name = "none"
}
var contactA = Contact()
var contactB = contactA
address(&contactA) // 0x10569c1e0
address(&contactB) // 0x10569c1f0
При копировании создается новый объект, соответственно и новый адрес в памяти. Механизм Copy-on-Write не применяется.
Копирование массива с объектом типа struct
Давайте посмотрим на другой пример:
import Foundation
func address(_ object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return NSString(format: "%p", address) as String
}
struct Contact {
var name = "none"
}
// 1
var arrayA = [Contact(name: "Ivan")]
var arrayB = arrayA
// 2
address(&arrayA) // 0x600002120290
address(&arrayB) // 0x600002120290
// 3
arrayB[0].name = "Petr"
// 4
print(arrayA[0].name) // Ivan
print(arrayB[0].name) // Petr
// 5
address(&arrayA) // 0x600002120290
address(&arrayB) // 0x600002117cb0
- Создаем массив A с объектом типа stuct. Массив B создаем копированием массива A.
- Так как мы использовали поверхностное копирование, то сработал механизм Copy-on-Write и наши массивы смотрят на одинаковый адрес в памяти.
- Далее изменяем свойство объекта в массиве B на Petr.
- Так как структура — тип значения, то изменить свойство name по ссылке не получится. Создается новый объект типа struct и свойством Petr.
- Соответственно, при изменении объекта одной коллекции происходит полное копирование второй и адреса в памяти будут разные.
Дополнительное чтение:
- Дополнительно о Copy-on-Write в iOS
- Value Type и Reference Type или чем стек отличается от кучи?
- Управление памятью в Swift
- Рисунки и тексты iOS помогут вам понять глубокое и поверхностное копирование
- Как работает массив в Swift
Больше вопросов для собеседований вы можете найти в нашем Telegram-боте: iOS Interview Bot или Telegram-канале: iOS Interview Channel