惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
美团技术团队
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
L
LangChain Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
腾讯CDC
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
有赞技术团队
有赞技术团队
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
博客园 - 司徒正美
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
C
Check Point Blog
小众软件
小众软件
V
Visual Studio Blog
V
V2EX
F
Full Disclosure
J
Java Code Geeks
MongoDB | Blog
MongoDB | Blog
罗磊的独立博客
人人都是产品经理
人人都是产品经理
量子位
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
博客园_首页
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
Recorded Future
Recorded Future
G
Google Developers Blog
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
爱范儿
爱范儿
Jina AI
Jina AI

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Copy-on-Write в Swift: подготовка к собеседованию
fort_croquet · 2026-06-27 · via Все публикации подряд на Хабре

Средний

16 мин

0

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


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