В чем разница между копированием массива и структуры?

Рассмотрим еще один вопрос, который спрашивают на 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)
  1. Создаем класс Contact, поддерживающий протокол NSCopying (для возможности глубокого копирования). Также, для реализации глубокого копирования нам необходимо реализовать метод copy(with: ).
  2. Для того чтобы наши массивы могли поддерживать глубокое копирование, нам необходимо реализовать расширение для массива, где с помощью функции map{} производим копирование каждого экземпляра. copy() — возвращает объект, возвращенный функцией copy(with: ).
  3. С функцией address() мы уже знакомы. Эта функция возвращает текстовое представления адреса в памяти, передаваемого в нее объекта.
  4. Далее создаем массив A и добавляем в него единственный объект типа Contact с именем Ivan.
  5. Сначала разберемся с поверхностным копированием. Создаем массив B присвоением массива A
  6. Сработал механизм Copy-on-Write, поэтому адреса этих массивов в памяти будут одинаковы.
  7. Изменяем единственный объект массива B. Меняем имя на Petr.
  8. Как мы видим. Оба массива распечатали одинаковое значение — Petr.
  9. Так же, адреса в памяти двух массивов остались одинаковыми.

Почему мы получили такой результат при печати объектов массивов? И почему адреса в памяти двух массивов не изменились?

В тот момент, когда мы произвели поверхностное копирование создался массив B и так как в силу вступает механизм Copy-on-Write, то адреса этих массивов в памяти будут одинаковы. Так как поверхностное копирование не создает новые объекты внутри массива, а только хранит ссылки на объекты внутри него, то массивы A и B хранят одинаковую ссылку на объект Contact(name: «Ivan»).

Поверхностное копирование массивов в swift

Поэтому, после того, как вы изменили объект в одном массиве по ссылке, его значение изменилось и в другом. И так как вы по факту не изменили объект одного из массива, то 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)
  1. Переключаем тип копирования на глубокое копирование.
  2. Можно заметить, что адреса массивов смотрят на разные области в памяти.
  3. При выводе объектов массивов получаем разные значения.
Глубокое копирование массивов в swift

Так как при глубоком копировании мы не храним ссылки, а заново создаем объекты массива (по факту добавляем во второй массив новые объекты), то механизм 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
  1. Создаем массив A с объектом типа stuct. Массив B создаем копированием массива A.
  2. Так как мы использовали поверхностное копирование, то сработал механизм Copy-on-Write и наши массивы смотрят на одинаковый адрес в памяти.
  3. Далее изменяем свойство объекта в массиве B на Petr.
  4. Так как структура — тип значения, то изменить свойство name по ссылке не получится. Создается новый объект типа struct и свойством Petr.
  5. Соответственно, при изменении объекта одной коллекции происходит полное копирование второй и адреса в памяти будут разные.

Дополнительное чтение:

Больше вопросов для собеседований вы можете найти в нашем Telegram-боте: iOS Interview Bot или Telegram-канале: iOS Interview Channel


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *