Copy-on-Write в Swift: подготовка к собеседованию

от автора

За годы я сидел по обе стороны стола: и как кандидат, и как собеседующий — в том числе на позиции в крупные продуктовые компании. И именно Copy-on-Write раз за разом оказывался той темой, на которой видно разницу между «слышал слово» и «понимаю механизм». Тема звучит обманчиво просто — «копируем только при записи», — но крупняк любит докапываться до формулировок: не «массив копируется по значению», а когда именно копируется буфер, что проверяется перед записью, почему у функции проверки именно такая сигнатура. Один неаккуратный оборот — и за него тут же цепляются уточняющим вопросом.

Сразу скажу про планку ожиданий, чтобы снять тревогу: на практике от кандидата редко хотят академически точного описания рантайма Swift до последнего бита. Хотят, чтобы вы держали в голове рабочую модель («struct снаружи, общий буфер с refcount внутри, копия на первой записи в разделяемый буфер») и могли её развернуть на пару уровней вглубь, не плавая в базовых понятиях вроде семантики значения и ссылки. Сидя по другую сторону стола, я отсекаю не тех, кто не знает внутренностей компилятора, а тех, кто путается в фундаменте и выдаёт заученные фразы, под которыми ничего нет. Поэтому статья идёт от фундамента к деталям: сначала то, что обязательно надо понимать, потом то, чем можно приятно удивить.

Вопрос-ловушка на 5 строк, с которого всё обычно и начинается:

var a = [1, 2, 3]var b = a          // сколько памяти выделилось под копию?b.append(4)        // а теперь?

Ответ «нисколько» на первой строке и «вот теперь — да» на второй — это и есть Copy-on-Write. Ниже разберём, как это устроено внутри, как написать свой CoW-тип руками, где он ломается, и чем всё это связано с фундаментальной разницей struct и class.

Статья построена в формате подготовки к собеседованию: сначала компактная шпаргалка с вопросами и ответами для быстрого повторения, затем детальный разбор с примерами и диаграммами.


Шпаргалка: 14 вопросов с короткими ответами

Что такое Copy-on-Write?

Оптимизация, при которой объект с value-семантикой физически делит хранилище (буфер) с другими копиями до тех пор, пока кто-то не попытается его изменить. В момент мутации, если буфер не уникален, делается реальная копия. Снаружи — поведение value-типа, по цене — почти как у reference-типа.

Зачем CoW вообще нужен?

Чтобы value-семантика Array, String, Dictionary, Set не стоила полной копии при каждом присваивании. Без CoW let b = a для массива на миллион элементов копировал бы миллион элементов. С CoW — копируется одно слово (указатель на буфер) + инкремент счётчика ссылок.

Какие типы в стандартной библиотеке используют CoW?

Array, ContiguousArray, String, Dictionary, Set, Data. Все они — struct-обёртки над внутренним буфером-классом, который и шарится между копиями.

Получают ли обычные struct CoW автоматически?

Нет. CoW не встроен в язык для произвольных структур. Это паттерн: struct, внутри которого лежит ссылка на класс-хранилище, плюс ручная проверка уникальности перед мутацией. Stdlib-типы реализуют его сами; свой тип получит CoW только если вы напишете его руками.

Какой ключевой API делает CoW возможным?

isKnownUniquelyReferenced(_:) — функция стандартной библиотеки. Принимает inout-ссылку на экземпляр класса и возвращает true, если на объект существует ровно одна сильная ссылка. На этой проверке держится решение «копировать буфер или можно писать на месте».

Почему isKnownUniquelyReferenced принимает inout?

Чтобы гарантировать эксклюзивный доступ к переменной на время проверки и не дать создать временную лишнюю ссылку, которая исказила бы счётчик. inout обеспечивает exclusive access (закон эксклюзивности доступа Swift) — без этого результат проверки был бы недетерминированным.

Работает ли isKnownUniquelyReferenced с Objective-C классами?

Нет. Для @objc-классов и объектов, пришедших по мосту (bridging) из Foundation, функция возвращает false. Она рассчитана на нативные Swift-классы.

Когда массив реально копируется?

Только при мутирующей операции (append, subscript-set, removeLast, …) И при условии, что буфер в этот момент разделяется более чем одной ссылкой. Если ссылка одна — мутация идёт на месте, без копии.

Спасает ли CoW массива от шаринга, если элементы — классы?

Нет. CoW копирует буфер массива, но элементы-классы копируются как ссылки (поверхностная копия). Оба массива после копии будут указывать на одни и те же объекты-классы. Это классический источник неожиданного общего состояния.

inout-параметр — это передача по ссылке?

Нет. inout — это copy-in / copy-out: значение копируется в функцию, изменяется, и копируется обратно при выходе. Семантически это не указатель. Для CoW-типа это означает: внутри функции буфер может оказаться уникальным (мутация на месте) либо нет.

Делает ли CoW тип потокобезопасным?

Нет. Проверка уникальности и последующая мутация не атомарны. Два потока, мутирующие общий Array, дают гонку данных и UB. CoW — про память и копирование, не про синхронизацию.

Чем reserveCapacity помогает CoW?

Резервирует ёмкость заранее, чтобы серия append не вызывала повторных реаллокаций буфера. На уникальном буфере убирает лишние копии при росте. Полезно в горячих циклах.

В чём фундаментальная разница struct и class?

struct — value-тип: при присваивании/передаче семантически копируется, у него нет идентичности (=== к нему неприменим), нет наследования, управляется не через ARC напрямую. class — reference-тип: копируется ссылка, есть идентичность, наследование, lifecycle через ARC (retain/release). CoW — это способ дать struct дешёвую value-семантику, заняв у class механизм подсчёта ссылок.

Чем Swift в этом смысле отличается от Objective-C?

В Objective-C по умолчанию доминирует reference-семантика: NSArray, NSString, любой NSObject — это указатели, копирование = копия указателя + retain, value-семантика достигается ручным -copy (через NSCopying). Swift сделал value-типы первоклассными и безопасными по умолчанию — а CoW нужен именно затем, чтобы эта повсеместная value-семантика не стоила дорого.


Детальный разбор

1. Фундамент: семантика значения против семантики ссылки

Прежде чем говорить о CoW, нужно зафиксировать, что вообще копируется при b = a. Это не про «struct против class» как синтаксис, а про поведение при присваивании и передаче.

Семантика значения (value semantics): при присваивании или передаче в функцию создаётся независимая копия. Изменение одной копии не видно другим. Так ведут себя struct, enum, кортежи.

Семантика ссылки (reference semantics): при присваивании копируется ссылка, обе переменные указывают на один и тот же объект. Изменение через одну ссылку видно через все. Так ведут себя class, замыкания, actor.

struct PointV { var x: Int }class  PointR { var x: Int; init(_ x: Int) { self.x = x } }var v1 = PointV(x: 1)var v2 = v1          // КОПИЯ значенияv2.x = 99print(v1.x, v2.x)    // 1 99 — независимыlet r1 = PointR(1)let r2 = r1          // копия ССЫЛКИr2.x = 99print(r1.x, r2.x)    // 99 99 — один объект

С этим связано важное различие — идентичность против равенства:

  • == (Equatable) — равны ли значения.

  • === — это один и тот же объект в памяти. Применим только к reference-типам; к struct его применить нельзя в принципе, потому что у значения нет идентичности.

Упрощённая (и не вполне точная) ментальная модель — «struct на стеке, class в куче». На практике value-тип, у которого есть поле-ссылка (например, struct с полем-String или полем-классом), живёт сложнее: сама структура может лежать на стеке, но её внутренний буфер — в куче. Именно эта ситуация и порождает CoW.

Любимый способ крупных компаний проверить, понимаете ли вы это «снаружи value — внутри куча» — задача на рекурсивные типы:

class A { var a: A? }            // ✅ компилируетсяstruct C { var c: C? }           // ❌ ошибка компиляцииstruct B { var b: [B]? }         // ✅ компилируется

class A живёт без проблем: ссылка имеет фиксированный размер (одно слово), сколько бы уровней вложенности ни было. struct C не компилируется — error: value type 'C' cannot have a stored property that recursively contains it: чтобы положить C целиком внутрь COptional<C> хранит C инлайн), компилятору нужно вычислить размер, который оказывается бесконечным. А вот struct B с полем [B]? компилируется — и это ключевой момент: Array хранит элементы в куче за указателем, поэтому само поле b занимает фиксированный размер независимо от содержимого. Массив здесь работает ровно как «прослойка с heap-буфером», разрывающая рекурсию, — та самая конструкция «value снаружи, ссылка на буфер внутри», на которой стоит и CoW. (Тот же эффект дают indirect enum и любая обёртка-класс.)

Грубая карта типов Swift:

Value semantics            Reference semantics─────────────────          ───────────────────struct                     classenum                       closure (функция-замыкание)tuple                      actorInt, Double, Bool          metatype классовArray, String, Dict, Set   ...(value снаружи, CoW-буфер внутри)

Обратите внимание на последнюю строку: коллекции — это value-типы снаружи, но внутри у них reference-хранилище. Это и есть гибрид, ради которого существует Copy-on-Write.

Врезка: чем Swift отличается от Objective-C. В Objective-C по умолчанию доминирует reference-семантика — NSArray, NSString, NSDictionary и вообще любой потомок NSObject представлены указателями, а присваивание NSArray *b = a; копирует указатель и делает retain, не значение. Чтобы получить независимую копию, программист обязан явно вызвать -copy/-mutableCopy (контракт NSCopying), и для immutable-объектов -copy часто вырождается в retain, а для mutable — в реальное копирование. Value-типов в ObjC по сути только сишные примитивы и struct из C. Swift перевернул умолчание: value-типы стали первоклассными и безопасными по умолчанию (Array, String, ваши struct), а чтобы эта повсеместная value-семантика не убивала производительность копированием, в стандартную библиотеку встроили Copy-on-Write. То есть CoW — это плата за то, что в Swift «по умолчанию всё копируется».

2. Какую проблему решает CoW

Возьмём наивную value-семантику без оптимизаций. Каждое присваивание массива — полная копия буфера:

var a = [Int](repeating: 0, count: 1_000_000)var b = a   // в наивной модели: malloc + копирование 8 МБvar c = b   // ещё 8 МБ

Три переменные — 24 МБ, хотя ни одну мы пока не меняли. Для коллекций, которые в Swift копируются на каждом шагу (присваивание, передача в функцию, возврат из функции, захват), это неприемлемо.

Противоположная крайность — сделать массив class (reference-семантикой) — ломает безопасность: две переменные начинают незаметно делить состояние, и мутация в одном месте «простреливает» в другое.

CoW — компромисс: вести себя как value (полная изоляция изменений), а платить как reference, пока никто не пишет. Цель — отложить реальную копию буфера до первой мутации и сделать её только если буфер действительно с кем-то разделён.

3. Анатомия CoW-типа: struct снаружи, класс-буфер внутри

Упрощённая модель того, как устроен Array:

// Внутреннее хранилище — КЛАСС (reference-тип), живёт в куче,// управляется ARC и имеет счётчик ссылок.final class ArrayBuffer {    var elements: UnsafeMutablePointer<Element>    var count: Int    var capacity: Int    // ...}// Сам Array — это STRUCT (value-тип) с единственным полем-ссылкой.struct Array<Element> {    var buffer: ArrayBuffer}

Что происходит на var b = a:

Шаг 1: var a = [1, 2, 3]    a (struct на стеке)    ┌─────────────────┐    │ buffer ─────────┼────► ArrayBuffer (куча), refcount = 1    └─────────────────┘      ┌──────────────────────────┐                             │ refcount: 1              │                             │ [1, 2, 3]                │                             └──────────────────────────┘Шаг 2: var b = a   (копируется struct — то есть одно поле-ссылка)    a ──────────────┐                    ├──► ArrayBuffer (куча), refcount = 2    b ──────────────┘    ┌──────────────────────────┐                         │ refcount: 2              │                         │ [1, 2, 3]                │                         └──────────────────────────┘

b = a скопировал не миллион элементов, а одно машинное слово (указатель buffer) и сделал ARC-инкремент счётчика буфера: refcount 1 → 2. Дёшево и постоянно по времени.

Теперь — мутация:

Шаг 3: b.append(4)   Проверка: буфер уникален? refcount == 2 → НЕТ.   Значит, прежде чем писать, клонируем буфер:    a ──────────────────► ArrayBuffer #1, refcount = 1                          │ [1, 2, 3]                │    b ──────────────────► ArrayBuffer #2, refcount = 1  (свежая копия)                          │ [1, 2, 3, 4]             │

a осталась [1, 2, 3], b стала [1, 2, 3, 4] — value-семантика соблюдена. Но копия буфера случилась ровно один раз и ровно тогда, когда понадобилась.

А если бы ссылка была единственной:

var a = [1, 2, 3]a.append(4)   // refcount == 1 → буфер уникален → пишем НА МЕСТЕ, без копии

Вот почему это называется Copy-on-Write: копия привязана не к присваиванию, а к первой записи в разделяемый буфер.

4. Сердце механизма: isKnownUniquelyReferenced

Решение «копировать или писать на месте» опирается на один вопрос: сколько сильных ссылок указывает на буфер прямо сейчас? Ответ даёт функция стандартной библиотеки:

func isKnownUniquelyReferenced<T: AnyObject>(_ object: inout T) -> Bool

Она возвращает true, если на переданный экземпляр класса существует ровно одна сильная ссылка (та самая, что вы передали).

final class Box {    var value: Int    init(_ value: Int) { self.value = value }}var box1 = Box(10)print(isKnownUniquelyReferenced(&box1))   // true — одна ссылкаvar box2 = box1                            // вторая сильная ссылкаprint(isKnownUniquelyReferenced(&box1))   // false — refcount == 2box2 = Box(20)                             // box1 снова уникаленprint(isKnownUniquelyReferenced(&box1))   // true

Три нюанса, которые любят на собеседовании:

Почему inout? Функции нужен эксклюзивный доступ к переменной, и она не должна сама создавать временную лишнюю ссылку (которая исказила бы счётчик до 2). inout гарантирует exclusive access по закону эксклюзивности доступа Swift и не плодит retain. Если бы сигнатура брала T по значению, на время вызова существовала бы вторая ссылка-аргумент, и функция всегда видела бы минимум две.

Только нативные классы. Для @objc-классов и объектов, пришедших по мосту (bridging) из Foundation, функция консервативно возвращает false. Поэтому свой CoW-буфер надо делать нативным Swift-классом (часто final).

weak/unowned ссылки не считаются. Функция смотрит на сильные ссылки. Слабая ссылка на тот же объект не сделает его «неуникальным».

5. Видим CoW глазами: адреса буфера и счётчик ссылок

Лучший способ убедиться, что буфер реально шарится, — посмотреть на адрес его хранилища до и после мутации.

func bufferAddress<T>(_ array: [T]) -> UnsafeRawPointer {    array.withUnsafeBufferPointer { UnsafeRawPointer($0.baseAddress!) }}var a = [1, 2, 3]var b = aprint(bufferAddress(a))   // 0x600000abc000print(bufferAddress(b))   // 0x600000abc000  ← ТОТ ЖЕ буфер, копии не былоb.append(4)print(bufferAddress(a))   // 0x600000abc000  ← a осталась на старом буфереprint(bufferAddress(b))   // 0x600000def000  ← b переехала на копию

(Конкретные адреса у вас будут свои, важно их совпадение/расхождение.)

То же самое можно подтвердить через счётчик ссылок. Для нативного класса есть «нечестный», но рабочий способ заглянуть в refcount в отладке:

final class Storage { var data = [Int]() }let s1 = Storage()print(CFGetRetainCount(s1))   // 2 — не 1! (рантайм держит временные retain'ы)let s2 = s1print(CFGetRetainCount(s1))   // 3 — на 1 больше

Обратите внимание: на единственной «логической» ссылке счётчик уже равен 2, а не 1 — рантайм держит временные retain’ы. Именно поэтому CFGetRetainCount — инструмент исключительно для исследования/отладки: абсолютное значение включает служебные retain’ы и не годится для логики в продакшене. Важна только разница (+1 на каждую новую ссылку). Для логики есть ровно один правильный инструмент — isKnownUniquelyReferenced.

6. Пишем свой CoW-тип руками

Теперь соберём всё вместе и реализуем собственный value-тип с Copy-on-Write. Это любимая «практическая» задача на senior-интервью.

// 1. Хранилище — КЛАСС. Именно его refcount мы будем проверять.private final class Storage {    var data: [Int]    init(_ data: [Int]) { self.data = data }    // удобный клон для копирования    func copy() -> Storage { Storage(data) }}// 2. Публичный тип — STRUCT с value-семантикой.struct CoWBuffer {    private var storage: Storage    init(_ data: [Int] = []) {        storage = Storage(data)    }    // Чтение — без копий, отдаём общий буфер.    var values: [Int] { storage.data }    // Любая мутация проходит через единую точку: ensureUnique().    private mutating func ensureUnique() {        if !isKnownUniquelyReferenced(&storage) {            print("⚙️ буфер разделён — делаем копию")            storage = storage.copy()        }    }    mutating func append(_ x: Int) {        ensureUnique()        storage.data.append(x)    }    mutating func update(at index: Int, to value: Int) {        ensureUnique()        storage.data[index] = value    }}

Поведение полностью повторяет стандартные коллекции:

var a = CoWBuffer([1, 2, 3])var b = a                 // делят один Storage, копии нетprint(a.values, b.values) // [1, 2, 3] [1, 2, 3]b.append(4)               // ⚙️ буфер разделён — делаем копиюprint(a.values)           // [1, 2, 3]      — не задетаprint(b.values)           // [1, 2, 3, 4]   — изменилась только bb.update(at: 0, to: 99)   // буфер b уже уникален — копии НЕ будет, пишем на местеprint(b.values)           // [99, 2, 3, 4]

Ключевые моменты, на которые смотрят на интервью:

  • Хранилище — обязательно class (нужен refcount), публичный тип — struct (нужна value-семантика).

  • Все мутирующие методы должны проходить через ensureUnique() до записи. Забыли в одном методе — словили общий стейт и нарушение value-семантики.

  • isKnownUniquelyReferenced(&storage) требует, чтобы storage был var и передавался inout — поэтому проверка живёт внутри mutating-метода.

7. Подводные камни — то, что отделяет middle от senior

7.1. struct с полем-классом: CoW вас не спасёт

CoW массива копирует буфер массива, но элементы копируются «как есть». Если элемент — класс, копируется ссылка, а не объект:

final class Node { var value = 0 }var a = [Node()]var b = a            // буфер скопируется при мутации, но Node — общийb[0].value = 42      // мутация ОБЪЕКТА, не буфераprint(a[0].value)    // 42 (!) — a и b делят один Node

Здесь b[0].value = 42 не меняет сам массив (его длину/состав), а мутирует объект по общей ссылке. CoW массива тут ни при чём — это поверхностная (shallow) копия. То же самое случается со struct, внутри которого лежит поле-класс: скопировав структуру, вы делите её внутренний объект.

7.2. Захват в замыкании ломает уникальность

Замыкание, захватившее переменную, держит дополнительную ссылку на буфер. Пока замыкание живо, буфер не уникален — и мутация массива даст лишнюю копию:

var data = [1, 2, 3]let printer = { print(data) }   // замыкание захватило data, +1 ссылка на буферdata.append(4)   // буфер не уникален → копия, хотя «логически» data одна

В горячем коде такие невидимые ссылки (замыкания, лишние временные переменные, передача в функции) приводят к копиям, которых не ждёшь.

7.3. inout — это не «по ссылке»

func mutate(_ arr: inout [Int]) {    arr.append(0)}

inout реализован как copy-in / copy-out: на входе значение копируется внутрь, на выходе — обратно. Это не указатель на оригинал. Для CoW важно следствие: внутри функции буфер обычно оказывается уникальным (внешняя ссылка на время вызова «заморожена» эксклюзивным доступом), поэтому мутация чаще идёт на месте.

7.4. CoW не делает тип потокобезопасным

Проверка уникальности и мутация — две отдельные операции, между ними нет атомарности:

var shared = [1, 2, 3]// Поток 1: shared.append(4)// Поток 2: shared.append(5)// → гонка данных на refcount/буфере, неопределённое поведение

Array/Dictionary/Set не потокобезопасны. CoW — про память и копирование, синхронизацию обеспечивайте сами (очередь, actor, блокировка).

7.5. Рост буфера, count vs capacity и reserveCapacity

Любимая задача-«что не так с этим кодом»:

var array = [0, 1, 2]for i in 3...10_000_000_000 {   // условно «много раз» добавляем в конец    array.append(i)}

Здесь полезно различать два понятия. count — сколько элементов в массиве сейчас. capacity — сколько влезет в текущий буфер до следующей реаллокации. Когда count упирается в capacity, append выделяет новый буфер (примерно вдвое больше), копирует туда все элементы и освобождает старый. За счёт геометрического роста средняя стоимость одного append остаётся амортизированной O(1): да, отдельные вставки дорогие (полное копирование), но они случаются всё реже, и в сумме на N вставок приходится ~2N копирований — то есть O(1) на элемент.

Реальную прогрессию легко увидеть, печатая capacity в момент её изменения (Swift 6, 64-бит):

count=1   capacity: 0 → 2count=3   capacity: 2 → 4count=5   capacity: 4 → 8count=9   capacity: 8 → 16count=17  capacity: 16 → 36count=37  capacity: 36 → 76count=77  capacity: 76 → 156...

Коэффициент роста — примерно ×2 (точная формула — деталь реализации stdlib и не гарантирована: как видно, после 16 это ближе к 2·n + 4). Важно не конкретное число, а сам принцип: ёмкость растёт мультипликативно, а не на +1 за раз, — иначе append был бы O(n) на каждую вставку и O(n²) на цикл.

Что цепляет интервьюер в этой задаче:

  • «Каждый ли append копирует массив?» — Нет. Реаллокация происходит только при исчерпании capacity, между ними append пишет в уже выделенный буфер. Плюс к этому каждая вставка проверяет уникальность буфера: если массив ни с кем не делится — пишем на месте, копии буфера нет вовсе.

  • «Что физически не так с этим циклом?»10^10 элементов по 8 байт — это ~80 ГБ. Код упрётся в память задолго до конца. Это вопрос на здравый смысл, а не на синтаксис.

  • «Как ускорить, если итоговый размер известен заранее?»reserveCapacity. Он выделяет буфер нужной ёмкости один раз и убирает промежуточные реаллокации:

var array = [0, 1, 2]array.reserveCapacity(1_000_000)   // одна аллокация вместо ~20 удвоенийfor i in 3..<1_000_000 { array.append(i) }

И обратная сторона роста — лишние копии, когда буфер вдобавок ещё и разделён:

func appendAll(to base: [Int], _ items: [Int]) -> [Int] {    var result = base       // делит буфер с base    for x in items {        result.append(x)    // первая итерация — копия (base ещё жива),    }                       // дальше — на месте    return result}

Первая мутация отвяжет result от base (одна CoW-копия), дальше пишем на месте — но реаллокации при росте всё равно будут, если не зарезервировать ёмкость заранее.

7.6. Мутация коллекции во время итерации

var elements = [1, 2, 3]for e in elements {    print(e)    elements = [4, 5, 6]   // переприсваиваем прямо внутри цикла}

Что выведет? — 1 2 3, а не 4 5 6. И это прямое следствие value-семантики плюс CoW.

for e in elements под капотом берёт у массива итератор (makeIterator()), а IndexingIterator для Array хранит собственную копию массива — то есть свою ссылку на буфер. Когда внутри цикла мы пишем elements = [4, 5, 6], мы лишь перенаправляем переменную elements на новый массив; копия, которую держит итератор, продолжает указывать на исходный буфер [1, 2, 3]. Цикл честно доходит до конца по старым данным.

Тот же исход будет и при мутации вместо переприсваивания:

var elements = [1, 2, 3]for e in elements {    print(e)    elements.append(99)    // мутация общего буфера}// напечатает 1 2 3; итератор работает по своей копии

append увидит, что буфер делится с итератором (не уникален), сделает CoW-копию для elements, а итератор останется на нетронутом исходном буфере. В Objective-C аналогичный трюк с NSMutableArray (reference-семантика) кинул бы NSGenericException: collection was mutated while being enumerated — потому что энумератор и массив делят один объект. В Swift коллекция — value-тип, и итерация защищена собственной копией: то, что в ObjC было рантайм-краш, в Swift стало предсказуемым и безопасным поведением.

8. Как это связано с struct vs class (и снова про Objective-C)

CoW — прямое следствие выбора Swift в пользу value-семантики по умолчанию. Соберём картину целиком.

struct (value): копируется значение, нет идентичности (=== неприменим), нет наследования, не управляется ARC напрямую — у самой структуры нет счётчика ссылок (он есть только у её внутренних полей-классов, если они есть). class (reference): копируется ссылка, есть идентичность, наследование, lifecycle через ARC.

CoW занимает у мира классов ровно одну вещь — счётчик ссылок ARC — и приклеивает её к value-типу через внутренний класс-буфер. Получается «struct снаружи, refcount внутри»: дешёвое копирование reference-типа плюс изоляция value-типа.

В Objective-C этой проблемы не было, потому что не было и повсеместной value-семантики: коллекции были классами (NSArray), копирование по умолчанию означало копию указателя, а независимую копию приходилось запрашивать вручную через -copy/NSCopying. Swift поменял умолчание на «копируется значение» — и, чтобы это умолчание не било по производительности, встроил Copy-on-Write в ключевые типы стандартной библиотеки. Иначе говоря: в ObjC вы платили за value-семантику явным -copy там, где она нужна; в Swift вы получаете её бесплатно везде, а CoW гарантирует, что «бесплатно» не превратится в «дорого».

9. Типичные вопросы на собеседовании

«Что выведет код?»

var a = [1, 2, 3]var b = ab.append(4)print(a, b)

[1, 2, 3] [1, 2, 3, 4]. На b = a копии нет (общий буфер), на append буфер не уникален → копия, мутируется только b.

«А этот?»

final class Node { var v = 0 }var a = [Node()]var b = ab[0].v = 7print(a[0].v)

7. CoW копирует буфер массива, но элемент — ссылка на общий Node. Это поверхностная копия.

«Получают ли мои собственные struct CoW автоматически?» — Нет. Это паттерн (struct + класс-буфер + isKnownUniquelyReferenced), а не фича компилятора. Stdlib-коллекции реализуют его вручную; ваш тип — только если вы напишете сами.

«Когда массив реально копирует буфер?» — При мутирующей операции и только если буфер в этот момент разделён (refcount > 1). Уникальный буфер мутируется на месте.

«Зачем isKnownUniquelyReferenced принимает inout — Для эксклюзивного доступа и чтобы не создать временную лишнюю ссылку, которая исказила бы счётчик.

«Делает ли CoW тип потокобезопасным?» — Нет. Проверка уникальности и мутация не атомарны; общий доступ из нескольких потоков — гонка данных.

«Чем inout отличается от передачи по ссылке?»inout — copy-in/copy-out, а не указатель. Семантически это значение, скопированное туда и обратно.

«Как CoW связан с разницей struct и class?» — CoW даёт value-типу (struct) дешёвое копирование, заняв у reference-типа (class) механизм подсчёта ссылок: внутри value-обёртки лежит class-буфер, чей refcount и проверяется перед записью.

«Чем Swift отличается от Objective-C в этом вопросе?» — ObjC по умолчанию reference-семантичен (копия = копия указателя + retain, value-копия вручную через -copy), Swift по умолчанию value-семантичен, и CoW — плата за то, чтобы это умолчание было дешёвым.

10. Чек-лист «что сказать на собесе про CoW»

  • CoW = делим буфер, пока не пишем; на первой записи в разделяемый буфер — реальная копия.

  • Работает на паре «struct-обёртка + class-буфер»; проверка — isKnownUniquelyReferenced(&storage).

  • Используют Array, String, Dictionary, Set, Data; обычные struct — только руками.

  • Копия привязана к записи, а не к присваиванию, и только если буфер не уникален.

  • Поверхностная копия: элементы-классы остаются общими.

  • Не потокобезопасен; inout — copy-in/copy-out, не ссылка.

  • Это следствие value-семантики Swift по умолчанию — в отличие от reference-умолчания Objective-C.


Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/articles/1052522/