Eingeführt mit Swift 5.0 zusammen mit der Unterstützung für die Bibliotheksevolution, wurde das @frozen-Attribut entwickelt, um die Spannung zwischen API-Erweiterbarkeit und binärer Stabilität zu lösen. Vor diesem Mechanismus waren alle öffentlichen Enums in resilienten Bibliotheken implizit nicht-frozen, was den Compiler zwang, anzunehmen, dass zukünftige Versionen möglicherweise unbekannte Fälle hinzufügen könnten. Diese Annahme verhinderte die Generierung kompakter, fester Layouts und erforderte defensive Programmiermuster im Client-Code. Das Attribut bietet eine formale Garantie, dass der Fallbestand des Enums für immer unveränderlich ist, was aggressive Optimierungen ermöglicht.
Das Problem entsteht, wenn eine Bibliothek ein Enum ohne dieses Attribut veröffentlicht. Swift muss das Enum dann als resilient betrachten und reserviert Speicherplatz in seiner Speicherrepräsentation, um zukünftige Fall-Diskriminatoren und assoziierte Wert-Layouts unterzubringen. Dies zwingt Client-Switches dazu, einen @unknown default-Fall zu enthalten, was die Kompilierzeitprüfung deaktiviert, dass alle logischen Zustände behandelt werden. Ohne einen solchen Standard würde das Hinzufügen eines Falls zur Bibliothek in vorkompilierten Client-Binärdateien, die nicht über den Code verfügen, um den neuen Diskriminatorwert zu verarbeiten, zu undefiniertem Verhalten führen, was zu Abstürzen oder Speicherbeschädigungen führen könnte.
Die Lösung liegt im @frozen-Annotation, die einen dauerhaften Vertrag etabliert. Indem ein Enum als frozen markiert wird, verspricht der Bibliotheksautor, dass sich die Menge der Fälle niemals ändern wird, was dem Compiler ermöglicht, feste ganzzahlige Tags zuzuweisen und ein stabiles, kompaktes Speicherlayout zu verwenden. Dies ermöglicht umfassende Switch-Anweisungen ohne Standardfälle, da der Compiler beweisen kann, dass alle möglichen Bitmuster des Diskriminators bekannten Fällen entsprechen. Die resultierende ABI-Stabilität sorgt dafür, dass die Größe und Ausrichtung des Enums über Bibliotheksversionen hinweg konstant bleibt, während der Client-Code von Sprungtabelle-Optimierungen und der verpflichtenden Handhabung jedes Zustands profitiert.
// Innerhalb einer mit -enable-library-evolution kompilierten Bibliothek @frozen public enum LoadState { case idle case loading case loaded(Data) } // Client-Code in einem separaten Modul func updateUI(for state: LoadState) { switch state { case .idle: print("Warten") case .loading: print("Spinner") case .loaded: print("Inhalt") // Compiler überprüft die Vollständigkeit; kein Standard erforderlich } }
Das Plattformteam eines Logistikunternehmens stellte ein Swift-Paket für die Routenoptimierung bereit, das ein TransportMode-Enum mit Fällen für .truck, .air und .ship verfügte. Da sie erwarteten, später .drone und .rail hinzuzufügen, veröffentlichten sie die Bibliothek zunächst ohne das @frozen-Attribut. Die Clientteams berichteten schnell, dass Xcode die Kompilierung von Switches ohne @unknown default-Klauseln verweigerte und logische Fehler verbarg, bei denen sie vergaßen, .ship in den Kostenberechnungen für Fracht zu behandeln.
Das Team erwog drei architektonische Ansätze, um dies zu lösen.
Erstens könnten sie den nicht-frozen Status beibehalten und in intensives Linting investieren, um sicherzustellen, dass Clients @unknown default-Handler schrieben, die Warnungen protokollierten. Dies bewahrte die Flexibilität, Transportmodi ohne größere Versionsänderungen hinzuzufügen, deaktivierte jedoch dauerhaft die Prüfung der Vollständigkeit zur Kompilierzeit. Es adressierte auch nicht den Überkopf der Binärgröße, da jede Enum-Instanz Resilienz-Metadaten trug, die die serialisierten Routenpakete an die Geräte der Fahrer aufblähten.
Zweitens könnten sie das Enum durch eine RawRepresentable-Struktur ersetzen, die von ganzzahligen Konstanten unterstützt wird. Dies würde ein fixes Speicherlayout bieten und das Hinzufügen neuer Modi ermöglichen, ohne die binäre Kompatibilität zu brechen, würde jedoch die Pattern-Matching-Fähigkeiten von Swift vollständig opfern. Entwickler wären gezwungen, in ausführlichen if-else-Ketten zu arbeiten, und der Compiler könnte nicht mehr überprüfen, dass alle möglichen Transportzustände in kritischen Pfadfindungsalgorithmen behandelt wurden.
Drittens könnten sie @frozen auf das Enum anwenden und sich auf die bestehenden drei Fälle festlegen, um einen separaten ExtendedTransportMode-Wrapper für zukünftige Erweiterungen zu erstellen. Dies würde den Resilienz-Überkopf beseitigen, die umfassende Switch-Kompilierung ermöglichen und garantieren, dass jeder Client alle aktuellen Modi explizit behandelt. Der Kompromiss war eine dauerhafte Einschränkung bei der Modifizierung des ursprünglichen Enums und die Notwendigkeit der Versionierung für grundlegende Ergänzungen.
Sie wählten die dritte Lösung. Nach dem Einfrieren von TransportMode entdeckten sie sofort zwei unbehandelte Switch-Fälle in ihrem eigenen Analytik-Dashboard während der Kompilierung. Die Entfernung der Resilienz-Metadaten reduzierte die Größe der übertragenen Routenobjekte um 18 %, und die explizite architektonische Grenze zwang zu einer saubereren Trennung zwischen der Kerntransportlogik und experimentellen Modi.
Warum bricht das Hinzufügen eines Falls zu einem nicht-frozen öffentlichen Enum die binäre Kompatibilität, selbst wenn der Quellcode des Clients weiterhin erfolgreich kompiliert?
Wenn Swift ein resilientes Modul kompiliert, nutzen nicht-frozen Enums eine variable Breite, die Platz für zukünftige Fall-Diskriminatoren reserviert. Wenn die Bibliothek anschließend einen Fall hinzufügt, ändert sich das Laufzeitlayout des Enums - zum Beispiel könnte der Diskriminator-Ganzzahlwert von 8 Bit auf 16 Bit erweitert werden, um das neue Tag aufzunehmen. Vorkompilierte Client-Binärdateien erwarten das alte Layout und enthalten Sprungtabellen oder bedingte Zweige, die nur den ursprünglichen Bereich der Tags berücksichtigen. Wenn diese Binärdateien den neuen Diskriminatorwert treffen, können sie ungültige Codepfade ausführen oder Speicher über die erwartete Payload-Grenze hinaus lesen, was zu Abstürzen führt, die Quellkode-@unknown default-Klauseln nicht verhindern können.
Wie interagiert @frozen mit Enums, die indirekte Fälle oder assoziierte Werte von resilienten Typen enthalten?
@frozen garantiert, dass die Identität und Anzahl der Fälle konstant bleibt, es friert jedoch nicht die Größe der assoziierten Werte ein. Wenn ein Fall eine Nutzlast eines nicht-frozen Structs oder eine Klassenreferenz trägt, bezieht sich die ABI-Stabilität des Enums auf das feste Diskriminator-Tag, während der Speicher für die Nutzlast weiterhin dynamische Größen über Zeiger oder Wertezeugtabellen verwenden kann. Kandidaten nehmen oft fälschlicherweise an, dass @frozen die gesamte Speicherbelegung, einschließlich der Nutzlastgrößen, fixiert; in Wirklichkeit gilt die Optimierung hauptsächlich für das Tag, und assoziierte Werte benötigen möglicherweise weiterhin Laufzeitanpassungsberechnungen, wenn ihre Typen selbst resilient sind oder unbekannte Größen enthalten.
Kann ein gefrorenes Enum innerhalb eines nicht-resilienten Moduls deklariert werden und was sind die langfristigen Auswirkungen, dies zu tun?
Ja, @frozen kann auf Enums innerhalb regulärer Anwendungstargets angewendet werden, in denen die Bibliotheksevolution deaktiviert ist. In diesem Kontext fungiert das Attribut als Dokumentation der Absicht, da alle Enums innerhalb des Moduls aufgrund der fehlenden Resilienzgrenzen effektiv gefroren sind. Kandidaten übersehen jedoch häufig, dass @frozen einen dauerhaften ABI-Vertrag darstellt; wenn das Modul später in ein resilientes Bibliotheks-Framework extrahiert wird, kann das Enum nicht ohne Verletzung der binären Kompatibilität mit bestehenden Clients eingefroren oder erweitert werden. Enums während der ursprünglichen Entwicklung explizit als gefroren zu kennzeichnen, schützt die Codebasis gegen versehentliche ABI-Verstöße, wenn sich die Architektur des Projekts weiterentwickelt.