För att bevara vanliga nästlade värden med en rekursiv Swift-enum implementerar du anpassad kodning och avkodning för varje värdetyp. Den här genomgången bygger en syntetisk Mac-appmodell som rundtrippas via Foundation-objekt helt i minnet, utan att lägga till omslag runt enum-fall.

Varför ändrar syntetiserad Codable representationen?

Anta att en Mac-datorapp har en liten layoutförhandsvisning med etiketter, numeriska värden, växlingsknappar, listor och nästlade grupper. En rekursiv enum kan beskriva dessa värden precist. Den konkreta uppgiften här är att konvertera den enumen till en vanlig objektgraf med Foundation-objekt och återskapa den samtidigt som de värdetyper som stöds bevaras.

Automatisk konformans ger inte den representation som krävs. Swifts implementerade design för enumsyntes placerar fallnamnet i en yttre nycklad container och placerar sedan tillhörande värden i en annan container. Tillhörande värden utan etiketter får genererade nycklar som _0. Därför får ett strängfall strukturella omslag i stället för att bli en vanlig sträng. Reglerna dokumenteras i Codable-syntes för enums med tillhörande värden.

Du kan byta namn på omslagen genom att ändra CodingKeys, men syntesen behåller sin strukturella modell. Den lösning som stöds är att själv implementera init(from:) och encode(to:). Apple dokumenterar uttryckligen manuell konformans för typer vars Swift-struktur skiljer sig från deras kodade struktur i Kodning och avkodning av anpassade typer.

Rekursion och representation är separata beslut. Swifts modifierare indirect möjliggör rekursiv enum-lagring; anpassade kodningsmetoder avgör den kodade formen. Språkhandboken förklarar hur du placerar indirect före enum-deklarationen för att tillämpa indirektion på dess tillhörande värdefall i Enumereringar.

Vad ska den vanliga representationen bevara exakt?

Använd följande kontrakt för det här exemplet. Det stöder medvetet ett litet urval värden som passar en syntetisk layoutförhandsvisning. Foundations dokumenterade konverteringsregler tillåter strängar, tal, arrayer, ordböcker och null-objekt, kräver strängar som ordboksnycklar och utesluter icke-ändliga tal. Se JSONSerialization.

Enum-fall Vanligt värde Kompatibilitetskrav
null NSNull Bevara ett uttryckligt null-värde
bool Booleskt värde Håll det åtskilt från numeriskt noll och ett
number Ändligt Double-värde Acceptera Double-typens precisionsbegränsningar
string Sträng Bevara text utan numerisk konvertering
array Array med vanliga värden Bevara elementens ordning och tomma arrayer
object Ordbok med strängnycklar Bevara nycklar, nästlade värden och tomma objekt

Kompatibilitet innebär här att bevara dessa kategorier och deras värden inom de angivna numeriska gränserna. Det krävs inte att en viss underklass av Foundation-tal bevaras eller att ordboksnycklarna visas i en viss ordning. En uttrycklig null-post måste också finnas kvar: ett objekt som innehåller "subtitle": NSNull() har en annan modellbetydelse än ett objekt utan nyckeln subtitle.

Innan du inför det här kontraktet bör du avgöra om tal verkligen kan samlas i ett enda Double-fall. Apple dokumenterar Double som en 64-bitars flyttalstyp och tillhandahåller initialiserare för exakt konvertering eftersom vanliga konverteringar kan avrunda. Om din modell behöver exakta stora heltal bör du välja en uttrycklig numerisk policy innan du konverterar dem. Om du lägger till ett heltalsfall behöver du också en avkodningsregel, eftersom vanliga numeriska värden inte innehåller Swift-enum-fallens identiteter. Se Double.

Hur implementerar du den rekursiva enumen?

Använd en aktuell Swift-miljö för Mac med Foundation tillgängligt. De följande tre kodblocken bildar i ordning ett komplett exempel på toppnivå: enumen, konverteringsfunktionerna och en körbar kontrollrutin. Alla exempelvärden skapas i minnet. Swift är inte tillgängligt i forskningsmiljön, så exemplet granskades men kördes inte.

Avkodaren kontrollerar först om värdet är null och försöker sedan med de primitiva typer som stöds. Apples SingleValueDecodingContainer tillhandahåller dessa operationer, inklusive decodeNil(). Den lokala hjälpfunktionen behandlar endast en typkonflikt som skäl att försöka med en annan primitiv typ; andra avkodningsfel skickas vidare.

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)
        }
    }
}

Varje kodningsgren skriver sin nyttolast direkt. Null-grenen anropar uttryckligen encodeNil(), och samlingsgrenarna använder arrayernas och ordböckernas kodningsimplementationer. Apple dokumenterar både primitiva och generiska kodningsoperationer i SingleValueEncodingContainer och samlingskonformans i Kodning och avkodning av anpassade typer.

Identifieringen av arrayer är avsiktligt åtskild från avkodningen av arrayelement. Om det aktuella värdet inte är en array kan en begäran om en unkeyed-container kasta ett typkonfliktfel. När en array väl har identifierats avkodas varje element med vanlig try, så ett elementfel skickas vidare i stället för att utlösa ett orelaterat försök att avkoda ett objekt. Det dokumenterade beteendet beskrivs i unkeyedContainer() och UnkeyedDecodingContainer.

Den här implementationen är avsedd för Foundations JSON-kodningsväg och den angivna uppsättningen värdetyper. Testa varje annan kodare eller avkodare mot samma kontrakt innan du använder den; konformans med Codable garanterar inte identiskt beteende mellan olika representationsformat.

Hur konverterar du till Foundation-objekt helt i minnet?

Foundation erbjuder två användbara gränssnitt: JSONEncoder skapar Data från ett kodningsbart värde och JSONDecoder återskapar ett avkodningsbart värde från Data. JSONSerialization konverterar mellan den mellanliggande representationen och Foundation-objekt. Genom att kombinera dessa dokumenterade operationer får du följande brygga i minnet. Se JSONEncoder, JSONDecoder och 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)
}

Mellanliggande Data ligger kvar i minnet. De här funktionerna utför serialiseringsarbete; de är inte nollkopieringskast mellan enumen och Foundation-samlingar. Begränsa konverteringen till den gräns där datorfunktionen faktiskt behöver en vanlig objektgraf, och behåll enumen för typade modelloperationer.

Båda alternativen .fragmentsAllowed är viktiga eftersom ett rekursivt värde kan ha en skalär rot. En sträng, boolesk typ, ett tal eller null på toppnivå behöver samma behandling som ett nästlat värde. Foundation dokumenterar det här beteendet separat för ReadingOptions.fragmentsAllowed och WritingOptions.fragmentsAllowed.

Den omvända funktionen accepterar Any eftersom det är Foundation-API:ets indatatyp, men dess domän är fortfarande begränsad till kontraktet ovan. Konverteringsfel skickas vidare till anroparen. Ersätt inte en misslyckad konvertering med ett tomt objekt och ta inte bort element som inte går att konvertera; det skulle ändra modellen utan att märkas.

Vilka kontroller visar kompatibilitet?

En rundresa ensam kan missa det ursprungliga problemet: kodning av en enum med syntes kan framgångsrikt rundtrippa sin egen omslutna struktur. Kontrollera både enum-jämlikheten och den vanliga formen. Följande rutin skapar en oberoende Foundation-graf, verifierar den förväntade enummodellen, kontrollerar de faktiska rotnycklarna i resultatet och provar varje rotkategori.

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()

Det förväntade framgångsmeddelandet är All compatibility checks passed. Kontroll av rotnycklar fångar ett tillagt yttre fältomslag. Den oberoende förväntade modellen fångar felaktiga val av fall, inklusive booleskt värde i stället för tal och text som liknar ett tal i stället för ett tal. Null-kontrollen bekräftar att subtitle finns kvar, medan rotkontrollerna omfattar skalära fragment och skiljer tomma arrayer från tomma objekt.

För en befintlig datormodell kan du anpassa den här diagnossekvensen till dess syntetiska värden: verifiera först den förväntade enumen, inspektera sedan den vanliga formen, återskapa därefter enumen och testa till sist gränsfall. Ge mellanresultaten namn så att ett misslyckat test visar vilken konvertering som ändrade värdet.

Vilka begränsningar har den här metoden?

Den numeriska policyn är den främsta begränsningen. Ett enda Double-fall kan inte garantera exakt bevarande av varje numeriskt Foundation-indatavärde. Om exakt heltalskonvertering spelar roll bör du validera att värdet kan representeras innan du för in det i den här modellen, eller utforma om de numeriska fallen och deras avkodningsregler. En lyckad serialisering visar inte att det numeriska värdet bevarats exakt. Apples Double-dokumentation skiljer mellan avrundade och exakta konverteringar.

Exemplet avvisar också uttryckligen icke-ändliga tal i båda kodningsriktningarna. Det håller enumens gräns i linje med de dokumenterade begränsningarna för den vanliga representationen med Foundation-objekt. Rekursiv struktur innebär inte obegränsat djup, så håll syntetiska modellträd rimligt stora och låt konverteringsfel nå den anropande funktionen.

För uppgiften med layoutförhandsvisningen blir resultatet en typad, rekursiv enum vars anpassade kodning bevarar vanliga strängar, booleska värden, ändliga tal, null-värden, arrayer och objekt med strängnycklar. Använd kontroller av formen tillsammans med rundresekontroller varje gång du ändrar fallen eller konverteringslogiken; tillsammans gör de den avsedda representationen konkret och möjlig att granska.

Källor