Introdotto con Swift 5.0 insieme al supporto per l'evoluzione delle librerie, l'attributo @frozen è stato progettato per risolvere la tensione tra l'estensibilità dell'API e la stabilità binaria. Prima di questo meccanismo, tutti gli enum pubblici nelle librerie resilienti erano implicitamente non congelati, costringendo il compilatore a presumere che versioni future potessero aggiungere casi sconosciuti. Questa assunzione impediva la generazione di layout compatti e di dimensione fissa e imponeva modelli di programmazione difensivi nel codice client. L'attributo fornisce una garanzia formale che l'inventario dei casi dell'enum è immutabile per sempre, consentendo ottimizzazioni aggressive.
Il problema sorge quando una libreria pubblica un enum senza questo attributo. Swift deve quindi trattare l'enum come resiliente, riservando spazio variabile nella sua rappresentazione in memoria per accomodare futuri discriminatori di caso e layout di valori associati. Ciò costringe gli switch del client a includere un caso @unknown default, disabilitando effettivamente la verifica a tempo di compilazione che tutti gli stati logici siano gestiti. Senza tale default, l'aggiunta di un caso alla libreria causerebbe comportamenti indefiniti nei binari client precompilati che non hanno il codice per elaborare il nuovo valore del discriminatore, portando a crash o corruzione della memoria.
La soluzione risiede nell'annotazione @frozen che stabilisce un contratto permanente. Contrassegnando un enum come congelato, l'autore della libreria promette che l'insieme di casi non cambierà mai, permettendo al compilatore di assegnare tag interi fissi e utilizzare un layout di memoria stabile e compatto. Questo consente switch esaurienti senza casi predefiniti, poiché il compilatore può dimostrare che tutti i possibili schemi di bit del discriminatore corrispondono a casi noti. La stabilità dell'ABI risultante garantisce che la dimensione e l'allineamento dell'enum rimangano costanti tra le versioni della libreria, mentre il codice client beneficia di ottimizzazioni della tabella dei salti e della gestione obbligatoria di ogni stato.
// All'interno di una libreria compilata con -enable-library-evolution @frozen public enum LoadState { case idle case loading case loaded(Data) } // Codice client in un modulo separato func updateUI(for state: LoadState) { switch state { case .idle: print("In attesa") case .loading: print("Caricamento") case .loaded: print("Contenuto") // Il compilatore verifica l'esaurimento; nessun default richiesto } }
Il team della piattaforma di un'azienda di logistica stava distribuendo un pacchetto Swift per l'ottimizzazione dei percorsi che esponeva un enum TransportMode con casi per .truck, .air, e .ship. Poiché prevedevano di aggiungere .drone e .rail nelle versioni successive, inizialmente distribuirono la libreria senza l'attributo @frozen. I team client hanno subito segnalato che Xcode si rifiutava di compilare switch senza clausole @unknown default, nascondendo errori di logica in cui avevano dimenticato di gestire .ship nei calcoli dei costi di spedizione.
Il team ha preso in considerazione tre approcci architetturali per risolvere questo problema.
Primo, potevano mantenere lo stato non congelato e investire in una pesante analisi statica per garantire che i client scrivessero gestori @unknown default che registrassero avvisi. Questo preservava la flessibilità di aggiungere modalità di trasporto senza aumenti di versione significativi, ma disabilitava permanentemente il controllo di esaurimento a tempo di compilazione. Non affrontava nemmeno il sovraccarico della dimensione binaria, poiché ogni istanza di enum portava metadati di resilienza che ingrandivano i pacchetti di percorso serializzati inviati ai dispositivi dei conducenti.
In secondo luogo, avrebbero potuto sostituire l'enum con una struct RawRepresentable supportata da costanti intere. Questo avrebbe fornito un layout di memoria fisso e consentito di aggiungere nuove modalità senza rompere la compatibilità binaria, ma avrebbe sacrificato completamente le capacità di pattern matching di Swift. Gli sviluppatori sarebbero stati costretti a catene verbose di if-else, e il compilatore non avrebbe potuto più verificare che tutti i possibili stati di trasporto fossero gestiti negli algoritmi critici di ricerca dei percorsi.
Terzo, avrebbero potuto applicare @frozen all'enum e impegnarsi per i tre casi esistenti, creando un wrapper separato ExtendedTransportMode per espansioni future. Questo avrebbe eliminato il sovraccarico di resilienza, consentito la compilazione esauriente degli switch e garantito che ogni client gestisse esplicitamente tutte le modalità attuali. Il compromesso era una restrizione permanente sulla modifica dell'enum originale e la necessità di versionare per eventuali aggiunte fondamentali.
Hanno scelto la terza soluzione. Dopo aver congelato TransportMode, hanno immediatamente scoperto due casi di switch non gestiti nel loro stesso dashboard analitico durante la compilazione. La rimozione dei metadati di resilienza ha ridotto la dimensione degli oggetti percorso trasmessi del 18%, e il confine architetturale esplicito ha costretto una separazione più pulita tra la logica di trasporto di base e le modalità sperimentali.
Perché aggiungere un caso a un enum pubblico non congelato interrompe la compatibilità binaria anche quando il codice sorgente del client si compila ancora correttamente?
Quando Swift compila un modulo resiliente, gli enum non congelati utilizzano una rappresentazione di larghezza variabile che riserva spazio per futuri discriminatori di caso. Se la libreria successivamente aggiunge un caso, il layout a runtime dell'enum cambia—ad esempio, l'intero discriminatore potrebbe espandersi da 8 bit a 16 bit per accomodare il nuovo tag. I binari client precompilati si aspettano il vecchio layout e contengono tabelle di salto o rami condizionali che considerano solo l'intervallo di tag originale. Quando questi binari incontrano il nuovo valore del discriminatore, potrebbero eseguire percorsi di codice non validi o leggere dalla memoria oltre il confine del carico previsto, causando crash che le clausole @unknown default a livello di sorgente non possono prevenire.
Come interagisce @frozen con gli enum che contengono casi indiretti o valori associati di tipi resilienti?
@frozen garantisce che l'identità e il conteggio dei casi rimangano costanti, ma non congela la dimensione dei valori associati. Se un caso porta un payload di una struct non congelata o una referenza a una classe, la stabilità dell'ABI dell'enum si riferisce al tag discriminatore fisso, mentre lo storage del payload può ancora utilizzare dimensioni dinamiche attraverso puntatori o tabelle di testimoni di valore. I candidati spesso presumono erroneamente che @frozen fissi l'intera impronta di memoria, comprese le dimensioni dei payload; in realtà, l'ottimizzazione si applica principalmente al tag, e i valori associati possono comunque richiedere calcoli di layout a runtime se i loro tipi sono a loro volta resilienti o contengono dimensioni sconosciute.
È possibile dichiarare un enum congelato all'interno di un modulo non resiliente, e quali sono le implicazioni a lungo termine di farlo?
Sì, @frozen può essere applicato a enum all'interno di obiettivi di applicazione regolari dove l'evoluzione delle librerie è disabilitata. In questo contesto, l'attributo funziona come documentazione di intento, poiché tutti gli enum all'interno del modulo sono effettivamente congelati a causa della mancanza di confini di resilienza. Tuttavia, i candidati spesso trascurano che @frozen costituisce un contratto ABI permanente; se il modulo viene in seguito estratto in un framework di libreria resiliente, l'enum non può essere scongelato o esteso senza rompere la compatibilità binaria con i client esistenti. Contrassegnare esplicitamente gli enum come congelati durante lo sviluppo iniziale protegge il codice contro violazioni accidentali dell'ABI quando l'architettura del progetto evolve.