За годы я сидел по обе стороны стола: и как кандидат, и как собеседующий — в том числе на позиции в крупные продуктовые компании. И именно 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 = 99
print(v1.x, v2.x) // 1 99 — независимы
let r1 = PointR(1)
let r2 = r1 // копия ССЫЛКИ
r2.x = 99
print(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 class
enum closure (функция-замыкание)
tuple actor
Int, 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 == 2
box2 = 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 = a
print(bufferAddress(a)) // 0x600000abc000
print(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 = s1
print(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] — изменилась только b
b.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 → 2
count=3 capacity: 2 → 4
count=5 capacity: 4 → 8
count=9 capacity: 8 → 16
count=17 capacity: 16 → 36
count=37 capacity: 36 → 76
count=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 = a
b.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 = a
b[0].v = 7
print(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.

























