Swift позволяет изменять значение на месте с помощью сочетания соглашений передачи параметров inout и функции времени выполнения isUniquelyReferenced. Когда вызывается mutating метод, компилятор преобразует вызов в параметр inout на уровне SIL, предоставляя методу исключительный доступ к памяти значения на время вызова. Перед модификацией какого-либо хранилища, выделенного в куче и разделяемого через ссылку на класс, время выполнения проверяет, равен ли счетчик ссылок точно одному, используя isUniquelyReferenced; если это так, он продолжает прямую модификацию, в противном случае создается защитная копия. Закон Исключительности, который обеспечивается с помощью статического анализа на этапе компиляции и динамического инструментария времени выполнения, гарантирует, что ни один другой поток или путь выполнения не могут получить доступ к значению в критический период проверки и модификации, предотвращая гонки состояний и поддерживая семантику значений без избыточных аллокаций.
Представьте, что вы разрабатываете высокопроизводительное приложение для редактирования фотографий, которое обрабатывает сырые данные изображений RAW с помощью пользовательской структуры ImageBuffer, оборачивающей массив байтов на 50 мегапикселей. Каждое применение фильтров — размытие, резкость или коррекция цвета — требует модификации миллионов пикселей, и пользователи ожидают реализацию в реальном времени при последовательном применении десяти и более корректировок без многосекундных задержек или сбоев памяти.
Одно из возможных решений заключалось в преобразовании ImageBuffer из struct в class, чтобы устранить накладные расходы на копирование за счет совместимого изменяемого состояния. Хотя этот подход предотвратил физическую дубликацию памяти во время цепочек фильтров, он ввел серьезные риски безопасности потоков, когда фоновый поток рендеринга одновременно обращался к буферу, и нарушал семантику значений, заставляя фильтры непреднамеренно изменять оригинальные данные изображения, которые делились через стек истории отмены.
Другим рассмотренным подходом было ручное глубокое копирование всего пиксельного буфера перед каждой операцией с фильтром, чтобы обеспечить полную изоляцию между этапами. Хотя эта стратегия сохраняла идеальные семантики значений и безопасность потоков, она приводила к катастрофическому снижению производительности — обработка одного изображения высокого разрешения через двенадцать фильтров требовала копирования сотен мегабайт памяти двенадцать раз, что приводило к многосекундной задержке и всплескам памяти, превышающим физические пределы устройства.
Выбранное решение реализовало семантику Copy-on-Write, используя частный класс Storage (финальный класс Swift), на который ссылается структура ImageBuffer. Каждый метод изменения фильтра сначала вызывал isUniquelyReferenced на экземпляре хранилища; в процессе последовательной обработки первое изменение инициировало копирование, в то время как последующие изменения того же экземпляра буфера выполнялись на месте без аллокации. Этот дизайн сохранил семантику значений Swift — позволяя безопасные операции отмены/повторения через эффективное копирование struct — при этом сохранял интерактивную производительность, избегая избыточной дубликации памяти во время цепочек фильтров.
В результате возник опыт редактирования, при котором пользователи могли применять двенадцать последовательных фильтров к изображениям высокого разрешения с откликами менее 100 миллисекунд и стабильным использованием памяти менее 200 МБ, по сравнению с предыдущими много гигабайтными пиковыми значениями памяти и зависаниями приложения, вызванными чрезмерным копированием.
Почему isUniquelyReferenced возвращает false для объектов Objective-C, даже когда только одна переменная Swift, кажется, удерживает ссылку?
Объекты Objective-C могут содержать "дополнительные" ссылки, невидимые для механизма подсчета ссылок Swift, такие как неретранованные ссылки из связанных объектов, регистрации в NSNotificationCenter или наблюдатели KVO. Функция isUniquelyReferenced в частности проверяет, равен ли счет сильных ссылок единице и является ли объект «чистым» нативным объектом Swift; для подклассов NSObject среда выполнения Objective-C может удерживать объект без обновления счетчика тем образом, который Swift может наблюдать, или объект может быть бессмертным (синглтон). Следовательно, Swift предоставляет isUniquelyReferencedNonObjC для явной обработки этого ограничения, хотя разработчики в целом должны гарантировать, что хранилища COW являются чистыми классами Swift, чтобы обеспечить точное обнаружение уникальности и избежать тихих регрессий производительности, когда копии происходят ненужно.
Как Закон Исключительности предотвращает гонки состояний во время проверки уникальности в конкурентных контекстах?
Закон Исключительности предписывает, что любой доступ к изменяемому значению должен быть эксклюзивным на протяжении этого доступа, что обеспечивается за счет сочетания статического анализа на этапе компиляции и динамического отслеживания времени выполнения с использованием инструментария проверки исключительности Swift. Когда метод mutating выполняет проверку isUniquelyReferenced, время выполнения уже установило запись эксклюзивного доступа для этого адреса памяти; если другой поток пытается читать или записывать значение в это время, нарушении исключительности будет обнаружено немедленно — либо на этапе компиляции для статических нарушений, либо с помощью механизма trap времени выполнения для динамических. Это предотвращает гонку состояний «проверить-затем-действовать», когда второй поток может увеличить счетчик ссылок между проверкой уникальности и самой модификацией, что в противном случае привело бы к двум потокам, изменяющим общий буфер одновременно, нарушая семантику значений и вызывая повреждение данных или сбои.
Почему хранилище COW должно быть реализовано как класс, а не как структура, и какой режим сбоя возникает, если используется структура?
Copy-on-Write требует совместимого изменяемого состояния для отслеживания, когда защитное копирование необходимо; только ссылочные типы (классы) обеспечивают идентичность объекта и совместимое подсчитывание ссылок во всех копиях обертки типа значения. Если разработчик по ошибке реализует хранилище как struct, каждое присвоение родительского типа создаст отдельную копию обертки хранилища, что означает, что поле счетчика ссылок будет дублировано, а не совместно использовано. Следовательно, isUniquelyReferenced всегда будет возвращать true для каждой копии независимо, что приведет к неправильному предположению о уникальности и выполнению модификаций на месте для буферов, которые логически являются общими, что приводит к ошибкам кросс-значений, когда изменение одного экземпляра struct неожиданно alters данные, видимые через другую, казалось бы, независимую переменную.