Per preservare i valori semplici annidati con un enum ricorsivo di Swift, implementa la codifica e la decodifica personalizzate per ogni tipo di valore. Questa guida realizza un modello sintetico per un’app Mac che effettua un round-trip tramite oggetti Foundation interamente in memoria, senza aggiungere wrapper per i casi dell’enum.
Perché Codable sintetizzato cambia la rappresentazione?
Supponiamo che un’app desktop per Mac abbia una piccola anteprima di layout contenente etichette, valori numerici, interruttori, elenchi e gruppi annidati. Un enum ricorsivo può descrivere con precisione questi valori. Il compito concreto consiste nel convertire quell’enum in un grafo semplice di oggetti Foundation e ricostruirlo preservando i tipi di valore supportati.
La conformità automatica non fornisce la rappresentazione richiesta. Il progetto implementato per la sintesi degli enum in Swift inserisce il nome del caso in un contenitore con chiavi esterno, quindi colloca i valori associati in un altro contenitore. Ai valori associati senza etichetta vengono assegnate chiavi generate, come _0. Di conseguenza, un caso stringa acquisisce wrapper strutturali invece di diventare una stringa semplice. Queste regole sono documentate in Codable synthesis for enums with associated values.
Modificare CodingKeys può rinominare questi wrapper, ma mantenere la sintesi significa conservarne il modello strutturale. La soluzione supportata consiste nell’implementare autonomamente init(from:) e encode(to:). Apple documenta esplicitamente la conformità manuale per i tipi la cui struttura Swift differisce da quella codificata in Encoding and Decoding Custom Types.
La ricorsione e la rappresentazione sono decisioni separate. Il modificatore indirect di Swift consente la memorizzazione degli enum ricorsivi; i metodi di codifica personalizzati determinano la forma codificata. La guida del linguaggio spiega come collocare indirect prima della dichiarazione dell’enum per applicare l’indirezione ai suoi casi con valori associati in Enumerations.
Che cosa deve preservare esattamente la rappresentazione semplice?
Per questo esempio, usa il contratto seguente. Supporta intenzionalmente un insieme ristretto di valori adatto a un’anteprima sintetica di layout. Le regole di conversione documentate da Foundation consentono stringhe, numeri, array, dizionari e oggetti null, richiedono chiavi stringa per i dizionari ed escludono i numeri non finiti. Consulta JSONSerialization.
| Caso dell’enum | Valore semplice | Requisito di compatibilità |
|---|---|---|
null |
NSNull |
Preservare un valore null esplicito |
bool |
Valore booleano | Distinguerlo dallo zero e dall’uno numerici |
number |
Valore Double finito |
Accettare i limiti di precisione di Double |
string |
Stringa | Preservare il testo senza conversioni numeriche |
array |
Array di valori semplici | Preservare l’ordine degli elementi e gli array vuoti |
object |
Dizionario con chiavi stringa | Preservare chiavi, valori annidati e oggetti vuoti |
In questo caso, compatibilità significa preservare queste categorie e i relativi valori entro i limiti numerici dichiarati. Non è necessario preservare una specifica sottoclasse numerica di Foundation né l’ordine di comparsa delle chiavi del dizionario. Deve essere preservata anche una voce null esplicita: un oggetto contenente "subtitle": NSNull() ha un significato diverso nel modello rispetto a un oggetto privo della chiave subtitle.
Prima di adottare questo contratto, stabilisci se i numeri possano davvero condividere un unico caso Double. Apple documenta Double come tipo in virgola mobile a 64 bit e fornisce inizializzatori a conversione esatta, perché le conversioni ordinarie possono arrotondare. Se il tuo modello richiede interi grandi esatti, scegli una politica numerica esplicita prima di convertirli. Anche l’aggiunta di un caso intero richiede una regola di decodifica, perché i valori numerici semplici non contengono l’identità dei casi dell’enum Swift. Consulta Double.
Come si implementa l’enum ricorsivo?
Usa un ambiente Swift attuale per Mac con Foundation disponibile. I tre blocchi di codice seguenti, nell’ordine, compongono un esempio completo al livello principale: l’enum, le funzioni di conversione e una routine di verifica eseguibile. Tutti i valori di esempio vengono creati in memoria. Swift non è disponibile nell’ambiente di ricerca, quindi l’esempio è stato esaminato ma non eseguito.
Il decoder verifica prima il null, poi prova i tipi primitivi supportati. Apple’s SingleValueDecodingContainer fornisce queste operazioni, incluso decodeNil(). L’helper locale considera solo un errore di mancata corrispondenza del tipo come motivo per provare un altro primitivo; gli altri errori di decodifica vengono propagati.
import Foundation
indirect enum PlainValue: Codable, Equatable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([PlainValue])
case object([String: PlainValue])
init(from decoder: Decoder) throws {
let scalar = try decoder.singleValueContainer()
if scalar.decodeNil() {
self = .null
return
}
func matching<T: Decodable>(_ type: T.Type) throws -> T? {
do {
return try scalar.decode(type)
} catch DecodingError.typeMismatch(_, _) {
return nil
}
}
if let value = try matching(Bool.self) {
self = .bool(value)
return
}
if let value = try matching(String.self) {
self = .string(value)
return
}
if let value = try matching(Double.self) {
guard value.isFinite else {
throw DecodingError.dataCorrupted(.init(
codingPath: decoder.codingPath,
debugDescription: "Expected a finite number."
))
}
self = .number(value)
return
}
var sequence: UnkeyedDecodingContainer?
do {
sequence = try decoder.unkeyedContainer()
} catch DecodingError.typeMismatch(_, _) {
sequence = nil
}
if var items = sequence {
var values: [PlainValue] = []
while !items.isAtEnd {
values.append(try items.decode(PlainValue.self))
}
self = .array(values)
return
}
self = .object(try scalar.decode([String: PlainValue].self))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .bool(let value):
try container.encode(value)
case .number(let value):
guard value.isFinite else {
throw EncodingError.invalidValue(value, .init(
codingPath: encoder.codingPath,
debugDescription: "Expected a finite number."
))
}
try container.encode(value)
case .string(let value):
try container.encode(value)
case .array(let values):
try container.encode(values)
case .object(let values):
try container.encode(values)
}
}
}
Ogni ramo di codifica scrive direttamente il proprio valore. Il ramo null chiama esplicitamente encodeNil(), mentre i rami delle collezioni delegano alle implementazioni di codifica di array e dizionari. Apple documenta sia le operazioni di codifica primitive sia quelle generiche in SingleValueEncodingContainer, e la conformità delle collezioni in Encoding and Decoding Custom Types.
Il riconoscimento degli array è intenzionalmente separato dalla decodifica dei loro elementi. Richiedere un contenitore non strutturato può generare un errore di mancata corrispondenza del tipo quando il valore corrente non è un array. Una volta riconosciuto l’array, ogni elemento viene decodificato con il normale try, quindi gli errori sugli elementi vengono propagati invece di avviare un tentativo di decodifica di un oggetto non pertinente. Il comportamento documentato è descritto in unkeyedContainer() e UnkeyedDecodingContainer.
Questa implementazione si rivolge al percorso di codifica JSON di Foundation e all’insieme di valori descritto. Prima di usarla, verifica qualsiasi altro encoder o decoder rispetto allo stesso contratto: la sola conformità a Codable non garantisce un comportamento identico tra diversi formati di rappresentazione.
Come si converte in oggetti Foundation interamente in memoria?
Foundation offre due utili punti di passaggio: JSONEncoder produce Data da un valore codificabile e JSONDecoder ricostruisce da Data un valore decodificabile. JSONSerialization converte tra questa rappresentazione intermedia e gli oggetti Foundation. La combinazione di queste operazioni documentate fornisce il seguente ponte interamente in memoria. Consulta JSONEncoder, JSONDecoder e JSONSerialization.
func foundationObject(from value: PlainValue) throws -> Any {
let bytes = try JSONEncoder().encode(value)
return try JSONSerialization.jsonObject(
with: bytes,
options: [.fragmentsAllowed]
)
}
func plainValue(from object: Any) throws -> PlainValue {
let bytes = try JSONSerialization.data(
withJSONObject: object,
options: [.fragmentsAllowed]
)
return try JSONDecoder().decode(PlainValue.self, from: bytes)
}
I dati intermedi restano in memoria. Queste funzioni eseguono operazioni di serializzazione; non sono cast a copia zero tra l’enum e le collezioni Foundation. Mantieni la conversione al punto di passaggio in cui la funzionalità desktop ha effettivamente bisogno di un grafo di oggetti semplice, conservando l’enum per le operazioni sul modello tipizzato.
Entrambe le opzioni .fragmentsAllowed sono importanti perché un valore ricorsivo può avere una radice scalare. Una stringa, un booleano, un numero o un null al livello principale richiedono lo stesso trattamento dei valori annidati. Foundation documenta separatamente questo comportamento per ReadingOptions.fragmentsAllowed e WritingOptions.fragmentsAllowed.
La funzione inversa accetta Any perché è il tipo di input dell’API Foundation, ma il dominio supportato resta quello del contratto precedente. Gli errori di conversione vengono propagati al chiamante. Non sostituire una conversione non riuscita con un oggetto vuoto né rimuovere gli elementi che non è possibile convertire: modificheresti silenziosamente il modello.
Quali verifiche dimostrano la compatibilità?
Un semplice round-trip può non rilevare il problema originale: la codifica sintetizzata degli enum può completare con successo il round-trip della propria struttura avvolta. Verifica sia l’uguaglianza degli enum sia la forma semplice. La routine seguente costruisce un grafo Foundation indipendente, verifica il modello enum atteso, controlla le chiavi effettive dell’oggetto radice e prova ogni categoria di radice.
func runExample() throws {
let plain: [String: Any] = [
"title": "Tile preview",
"enabled": true,
"spacing": 8.5,
"subtitle": NSNull(),
"tiles": [
["label": "Circle", "size": 24.0],
["label": "Square", "size": 32.0]
]
]
let expected: PlainValue = .object([
"title": .string("Tile preview"),
"enabled": .bool(true),
"spacing": .number(8.5),
"subtitle": .null,
"tiles": .array([
.object([
"label": .string("Circle"),
"size": .number(24)
]),
.object([
"label": .string("Square"),
"size": .number(32)
])
])
])
let model = try plainValue(from: plain)
precondition(model == expected)
let output = try foundationObject(from: model)
guard let object = output as? [String: Any] else {
preconditionFailure("Expected a plain object root.")
}
precondition(Set(object.keys) == Set(plain.keys))
precondition(object["title"] as? String == "Tile preview")
precondition(object["subtitle"] is NSNull)
precondition((object["tiles"] as? [Any])?.count == 2)
let restored = try plainValue(from: output)
precondition(restored == expected)
let roots: [PlainValue] = [
.null, .bool(false), .bool(true),
.number(0), .number(1), .number(8.5),
.string("1"), .array([]), .object([:]),
expected
]
for root in roots {
let graph = try foundationObject(from: root)
let decoded = try plainValue(from: graph)
precondition(decoded == root)
}
do {
_ = try foundationObject(from: .number(.infinity))
preconditionFailure("Expected nonfinite number rejection.")
} catch EncodingError.invalidValue(_, _) {
// Expected: this model requires finite numbers.
}
print("All compatibility checks passed.")
}
try runExample()
Il messaggio di esito previsto è All compatibility checks passed. Il controllo delle chiavi della radice rileva l’aggiunta di un wrapper esterno per il caso. Il modello atteso indipendente intercetta la selezione di un caso errato, compresi booleani al posto di numeri e testo dall’aspetto numerico al posto di numeri. Il controllo del null conferma che subtitle rimane presente, mentre i controlli della radice coprono i frammenti scalari e distinguono gli array vuoti dagli oggetti vuoti.
Per un modello desktop esistente, adatta questa sequenza diagnostica ai suoi valori sintetici: verifica prima l’enum previsto, poi ispeziona la forma semplice, quindi ricostruisci l’enum e infine prova i casi limite. Assegna nomi ai risultati intermedi in modo che una verifica non riuscita identifichi quale conversione ha modificato il valore.
Quali sono i limiti di questo approccio?
Il contratto numerico è il limite principale. Un singolo caso Double non può garantire la preservazione esatta di ogni valore numerico Foundation in ingresso. Quando è importante una conversione intera esatta, convalida la rappresentabilità prima di usare questo modello oppure riprogetta i casi numerici e le relative regole di decodifica. Una serializzazione riuscita non dimostra che i valori numerici siano stati preservati esattamente. La documentazione di Double di Apple distingue le conversioni arrotondate da quelle esatte.
L’esempio rifiuta esplicitamente anche i numeri non finiti in entrambe le direzioni di codifica. In questo modo il confine dell’enum resta allineato con le restrizioni documentate della rappresentazione semplice di Foundation. Una struttura ricorsiva non implica una capacità di annidamento illimitata: mantieni gli alberi del modello sintetico di dimensioni ragionevoli e lascia che gli errori di conversione raggiungano la funzionalità chiamante.
Per l’attività dell’anteprima di layout, il risultato finale è un enum ricorsivo tipizzato la cui codifica personalizzata preserva stringhe semplici, valori booleani, numeri finiti, valori null, array e oggetti con chiavi stringa. Usa i controlli della forma insieme a quelli del round-trip ogni volta che modifichi i casi o la logica di conversione; insieme rendono concreta e verificabile la rappresentazione prevista.
Fonti
- Codable synthesis for enums with associated values — Swift Project.
- Enumerations — Swift Project.
- Encoding and Decoding Custom Types — Apple.
- SingleValueDecodingContainer — Apple.
- SingleValueEncodingContainer — Apple.
- unkeyedContainer() — Apple.
- UnkeyedDecodingContainer — Apple.
- JSONEncoder — Apple.
- JSONDecoder — Apple.
- JSONSerialization — Apple.
- ReadingOptions.fragmentsAllowed — Apple.
- WritingOptions.fragmentsAllowed — Apple.
- Double — Apple.