Введенный с Swift 5.0 и поддержкой эволюции библиотек, атрибут @frozen был создан для разрешения напряженности между расширяемостью API и бинарной стабильностью. До появления этого механизма все публичные перечисления в устойчивых библиотеках были неявно не зафиксированы, что заставляло компилятор предполагать, что будущие версии могут добавлять неизвестные случаи. Это предположение препятствовало генерации компактных компоновок фиксированного размера и обязывало клиентский код использовать защитные конструкции программирования. Атрибут предоставляет формальную гарантию того, что инвентаризация случаев перечисления будет неизменной навсегда, что позволяет проводить агрессивные оптимизации.
Проблема возникает, когда библиотека публикует перечисление без этого атрибута. Swift должен будет тогда рассматривать перечисление как устойчивое, резервируя переменное пространство в его представлении в памяти для учета возможных будущих дискримиаторов случаев и связанных с ними компоновок значений. Это заставляет клиентские switch включать случай @unknown default, что фактически отключает проверку на этапе компиляции, что все логические состояния обработаны. Без такого значения по умолчанию добавление случая в библиотеку вызвало бы неопределенное поведение в предварительно скомпилированных клиентских бинарниках, которые не содержат кода для обработки нового значения дискраминации, что приводит к сбоям или повреждению памяти.
Решение заключается в атрибуте @frozen, который устанавливает постоянный контракт. Обозначив перечисление как зафиксированное, автор библиотеки обещает, что набор случаев никогда не изменится, позволяя компилятору присваивать фиксированные целочисленные метки и использовать стабильную, компактную компоновку в памяти. Это позволяет использовать исчерпывающие выражения switch без значений по умолчанию, так как компилятор может доказать, что все возможные битовые шаблоны дискраминации соответствуют известным случаям. Результирующая стабильность ABI гарантирует, что размер и выравнивание перечисления остаются постоянными между версиями библиотеки, в то время как клиентский код пользуется оптимизацией через таблицы переходов и обязательной обработкой каждого состояния.
// Внутри библиотеки, скомпилированной с -enable-library-evolution @frozen public enum LoadState { case idle case loading case loaded(Data) } // Клиентский код в отдельном модуле func updateUI(for state: LoadState) { switch state { case .idle: print("Ожидание") case .loading: print("Индикатор загрузки") case .loaded: print("Содержимое") // Компилятор проверяет все случаи; значение по умолчанию не требуется } }
Команда платформы в логистической компании разрабатывала пакет Swift для оптимизации маршрутов, который экспонировал перечисление TransportMode с случаями для .truck, .air и .ship. Поскольку они ожидали добавить .drone и .rail в последующих выпусках, они изначально распространили библиотеку без атрибута @frozen. Команды клиентов вскоре сообщили, что Xcode отказывался компилировать switch без @unknown default, скрывая логические ошибки, в которых они забыли обработать .ship в расчетах затрат на грузоперевозки.
Команда рассмотрела три архитектурных подхода для решения этой проблемы.
Первый, они могли бы сохранить статус не зафиксированного и инвестировать в жесткую проверку, чтобы убедиться, что клиенты писали обработчики @unknown default, которые регистрировали предупреждения. Это сохранило бы гибкость в добавлении транспортных средств без значительных изменений версии, но навсегда отключило бы проверку заключительности на этапе компиляции. Это также не решило бы проблему с увеличением бинарного размера, так как каждый экземпляр перечисления нес бы метаданные устойчивости, что увеличивало объем сериализованных пакетов маршрутов, отправляемых на устройства водителей.
Второй вариант заключался в том, чтобы заменить перечисление на структуру RawRepresentable, основанную на целых константах. Это обеспечивало бы фиксированную компоновку памяти и позволяло бы добавлять новые режимы без разрушения бинарной совместимости, но жертвуя полностью возможностями сопоставления шаблонов Swift. Разработчики были бы вынуждены использовать многословные цепочки if-else, и компилятор больше не мог бы гарантировать, что все возможные состояния транспортировки были обработаны в критических алгоритмах поиска пути.
Третий вариант зависел от применения @frozen к перечислению и приверженности к существующим трем случаям, создавая отдельный обертку ExtendedTransportMode для будущих расширений. Это устраняло бы накладные расходы по устойчивости, позволяло бы компиляцию исчерпывающих switch и обеспечивало бы, что каждый клиент явно обрабатывал все текущие режимы. Компромисс заключался в постоянном ограничении изменения оригинального перечисления и необходимости версионирования для любых фундаментальных дополнений.
Они выбрали третье решение. После заморозки TransportMode они немедленно обнаружили два необработанных случая switch в своей аналитической панели во время компиляции. Устранение метаданных устойчивости снизило размер передаваемых объектов маршрута на 18%, и явная архитектурная граница обеспечила более чистое разделение между основной логикой транспортировки и экспериментальными режимами.
Почему добавление случая в не замороженное публичное перечисление разрушает бинарную совместимость, даже когда исходный код клиента все еще успешно компилируется?
Когда Swift компилирует устойчивый модуль, не замороженные перечисления используют представление переменной ширины, которое резервирует пространство для будущих дискримиаторов случаев. Если библиотека впоследствии добавляет случай, компоновка перечисления изменяется: например, целое число дискраминации может расшириться с 8 бит до 16 бит, чтобы учесть новый тег. Предварительно скомпилированные клиентские бинарники ожидают старой компоновки и содержат таблицы переходов или условные ветви, которые учитывают только первоначальный диапазон тегов. Когда эти бинарники сталкиваются с новым значением дискраминации, они могут выполнять недопустимые пути кода или читать память за пределами ожидаемой границы нагрузки, что приводит к сбоям, которые не могут быть предотвращены путем использования @unknown default в исходном коде.
Как @frozen взаимодействует с перечислениями, содержащими косвенные случаи или связанные значения устойчивых типов?
@frozen гарантирует, что идентичность и количество случаев остаются постоянными, но не замораживает размер связанных значений. Если случай несет полезную нагрузку не замороженной структуры или ссылки на класс, стабильность ABI перечисления относится к фиксированному тегу дискраминации, в то время как хранение полезной нагрузки может по-прежнему использовать динамическое изменение размера через указатели или таблицы свидетельств значений. Кандидаты часто неправильно предполагают, что @frozen фиксирует всю память, включая размеры полезной нагрузки; на самом деле, оптимизация в первую очередь применяется к тегу, и связанные значения могут все равно требовать вычислений компоновки во время выполнения, если их типы сами являются устойчивыми или содержат неизвестные размеры.
Можно ли объявить замороженное перечисление в неустойчивом модуле и какие будут долгосрочные последствия этого?
Да, @frozen можно применить к перечислениям в обычных целевых приложениях, где эволюция библиотеки отключена. В этом контексте атрибут функционирует как документирование намерений, поскольку все перечисления в модуле фактически заморожены из-за отсутствия границ устойчивости. Однако кандидаты часто упускают из виду, что @frozen является постоянным контрактом ABI; если модуль позже извлечен в устойчивый фреймворк библиотеки, перечисление нельзя будет разморозить или расширить без разрушения бинарной совместимости с существующими клиентами. Явная маркировка перечислений как замороженные на начальной стадии разработки защищает кодовую базу от случайных нарушений ABI, когда архитектура проекта эволюционирует.