За годы я сидел по обе стороны стола: и как кандидат, и как собеседующий — в том числе на позиции в крупные продуктовые компании. И именно 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 целиком внутрь C (а Optional<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/