Wprowadzony w Swift 5.0 wraz z obsługą ewolucji biblioteki, atrybut @frozen został zaprojektowany w celu rozwiązania napięcia między rozszerzalnością API a stabilnością binarną. Przed wprowadzeniem tego mechanizmu wszystkie publiczne enumy w odpornych bibliotekach były domyślnie niezamrożone, zmuszając kompilator do zakupu założenia, że przyszłe wersje mogą dodać nieznane przypadki. To założenie uniemożliwiało generowanie kompaktowych układów o stałym rozmiarze i wymuszało stosowanie defensywnych wzorców programowania w kodzie klientów. Atrybut zapewnia formalną gwarancję, że zasoby przypadków enumu są niezmienne na zawsze, co umożliwia agresywne optymalizacje.
Problem pojawia się, gdy biblioteka publikuje enum bez tego atrybutu. Swift musi wtedy traktować enum jako odporny, rezerwując miejsce w pamięci na reprezentację, aby pomieścić przyszłe dyskryminatory przypadków i związane układy wartości. Zmusza to klienta do uwzględnienia przypadku @unknown default w switchu, co efektywnie wyłącza weryfikację na etapie kompilacji, że wszystkie możliwe stany logiczne są obsłużone. Bez takiego domyślnego przypadku dodanie przypadku do biblioteki spowoduje nieokreślone zachowanie w wstępnie skompilowanych binarnych klientach, które nie mają kodu do obsługi nowej wartości dyskryminatora, co prowadzi do awarii lub uszkodzenia pamięci.
Rozwiązanie leży w adnotacji @frozen, która ustanawia trwałą umowę. Oznaczając enum jako zamrożony, autor biblioteki obiecuje, że zbiór przypadków nigdy się nie zmieni, co pozwala kompilatorowi przypisać stałe tagi całkowite i używać stabilnego, kompaktowego układu pamięci. To umożliwia wyczerpujące switch statements bez przypadków domyślnych, ponieważ kompilator może wykazać, że wszystkie możliwe wzory bitowe dyskryminatora odpowiadają znanym przypadkom. Uzyskana stabilność ABI zapewnia, że rozmiar i wyrównanie enumu pozostają stałe w różnych wersjach biblioteki, podczas gdy kod klienta korzysta z optymalizacji tabeli skoków i obowiązkowej obsługi każdego stanu.
// W ramach biblioteki skompilowanej z -enable-library-evolution @frozen public enum LoadState { case idle case loading case loaded(Data) } // Kod klienta w osobnym module func updateUI(for state: LoadState) { switch state { case .idle: print("Czekam") case .loading: print("Spinner") case .loaded: print("Zawartość") // Kompilator weryfikuje wyczerpującość; nie wymaga domyślnego } }
Zespół platformowy w firmie logistycznej wdrażał pakiet Swift do optymalizacji tras, który ujawniał enum TransportMode z przypadkami .truck, .air i .ship. Ponieważ przewidywali dodanie .drone i .rail w kolejnych wydaniach, początkowo dystrybuowali bibliotekę bez atrybutu @frozen. Zespoły klientów szybko zgłaszały, że Xcode odmawia kompilacji switchy bez klauzul @unknown default, ukrywając błędy logiczne, gdzie zapomniano o obsłudze .ship w obliczeniach kosztów frachtu.
Zespół rozważał trzy podejścia architektoniczne, aby to rozwiązać.
Po pierwsze, mogliby zachować status niezamrożony i zainwestować w intensywne linting, aby zapewnić, że klienci pisali obsługiwacze @unknown default, które logowały ostrzeżenia. To zachowało elastyczność dodawania trybów transportu bez dużych wersji, ale na stałe wyłączyło weryfikację wyczerpującości na etapie kompilacji. Nie rozwiązało to również problemu nadmiaru rozmiaru binarnego, ponieważ każda instancja enumu niosła metadane odporności, co zwiększało rozmiar pakietów tras wysyłanych do urządzeń kierowców.
Po drugie, mogliby zastąpić enum strukturą RawRepresentable opartą na stałych całkowitych. To zapewniłoby stały układ pamięci i pozwoliłoby na dodawanie nowych trybów bez naruszania zgodności binarnej, ale zupełnie poświęciłoby możliwości dopasowywania wzorców Swift. Programiści byliby zmuszeni do używania rozbudowanych łańcuchów if-else, a kompilator nie mógłby już zweryfikować, że wszystkie możliwe stany transportowe są obsługiwane w krytycznych algorytmach wyszukiwania.
Po trzecie, mogliby zastosować @frozen do enumu i zobowiązać się do istniejących trzech przypadków, tworząc oddzielny wrapper ExtendedTransportMode dla przyszłych rozszerzeń. To wyeliminowałoby nadmiar odporności, umożliwiłoby wyczerpującą kompilację switch oraz gwarantowałoby, że każdy klient obsługiwałby wszystkie aktualne tryby w sposób jawny. Kosztem było nałożenie stałego ograniczenia na modyfikowanie oryginalnego enumu i konieczność wersjonowania dla jakichkolwiek fundamentalnych dodatków.
Wybrali trzecie rozwiązanie. Po zamrożeniu TransportMode od razu odkryli dwa nieobsłużone przypadki switch w swoim własnym dashboardzie analitycznym podczas kompilacji. Usunięcie metadanych odporności zmniejszyło rozmiar przesyłanych obiektów tras o 18%, a wyraźna granica architektoniczna wymusiła czystszy podział między rdzennej logiki transportowej a trybami eksperymentalnymi.
Dlaczego dodanie przypadku do publicznego enumu, który nie jest zamrożony, łamie zgodność binarną, nawet gdy kod źródłowy klienta wciąż się kompiluje?
Gdy Swift kompiluje odporny moduł, enumy niezamrożone wykorzystują zmienną długość reprezentacji, która rezerwuje miejsce na przyszłe dyskryminatory przypadków. Jeśli biblioteka następnie doda przypadek, układ enumu w czasie wykonywania zmienia się — na przykład, dyskryminator całkowity może rozszerzyć się z 8 bitów do 16 bitów, aby pomieścić nowy tag. Prekompilowane binarne pliki klientów oczekują starego układu i zawierają tabele skoków lub warunkowe gałęzie, które uwzględniają tylko oryginalny zakres tagów. Gdy te binaria napotykają nową wartość dyskryminatora, mogą uruchomić nieprawidłowe ścieżki kodu lub odczytać pamięć poza przewidywaną granicą ładunku, powodując awarie, których klauzule @unknown default na poziomie źródłowym nie mogą zapobiec.
Jak @frozen współdziała z enumami, które zawierają przypadki pośrednie lub wartości związane z typami odpornymi?
@frozen gwarantuje, że tożsamość i liczba przypadków pozostają stałe, ale nie zamraża rozmiaru związanych wartości. Jeśli przypadek niesie ładunek niezamrożonej struktury lub odniesienie do klasy, stabilność ABI enumu odnosi się do stałego tagu dyskryminatora, podczas gdy przechowywanie ładunku może wciąż wykorzystywać dynamiczne rozmiarowanie przez wskaźniki lub tabele świadków wartości. Kandydaci często błędnie zakładają, że @frozen blokuje całą stopę pamięci, w tym rozmiary ładunków; w rzeczywistości, optymalizacja dotyczy głównie tagu, a związane wartości mogą wciąż wymagać obliczeń układu w czasie wykonywania, jeśli ich typy są same w sobie odporne lub mają nieznane rozmiary.
Czy zamrożony enum może być zadeklarowany w nierezydentnym module, a jakie są długoterminowe skutki tego działania?
Tak, @frozen może być stosowany do enumów w regularnych celach aplikacji, gdzie ewolucja biblioteki jest wyłączona. W tym kontekście atrybut działa jako dokumentacja zamiaru, ponieważ wszystkie enumy w module są zasadniczo zamrożone z powodu braku granic odporności. Jednak kandydaci często przeoczają, że @frozen stanowi trwałą umowę ABI; jeśli moduł zostanie później wyodrębniony do odpornej biblioteki, enum nie może zostać odzamrożony ani rozszerzony bez łamania zgodności binarnej z istniejącymi klientami. Wyraźne oznaczenie enumów jako zamrożonych podczas początkowego rozwoju zabezpiecza kod przed przypadkowymi naruszeniami ABI w miarę ewolucji architektury projektu.