Um einfache verschachtelte Werte mit einem rekursiven Swift-Enum zu erhalten, implementiere für jede Wertart eine eigene Kodierung und Dekodierung. Diese Anleitung erstellt ein synthetisches Mac-App-Modell, das vollständig im Arbeitsspeicher über Foundation-Objekte hinweg verlustfrei hin- und zurückkonvertiert wird, ohne Enum-Fall-Wrapper hinzuzufügen.

Warum ändert die synthetisierte Codable-Konformität die Darstellung?

Angenommen, eine Mac-Desktop-App enthält eine kleine Layoutvorschau mit Beschriftungen, Zahlenwerten, Schaltern, Listen und verschachtelten Gruppen. Ein rekursives Enum kann diese Werte präzise beschreiben. Die konkrete Aufgabe besteht hier darin, dieses Enum in einen einfachen Foundation-Objektgraphen umzuwandeln und ihn wiederherzustellen, wobei die unterstützten Wertarten erhalten bleiben.

Die automatische Konformität liefert nicht die erforderliche Darstellung. Swifts implementiertes Enum-Synthesedesign platziert den Fallnamen in einem äußeren Keyed Container und die zugehörigen Werte anschließend in einem weiteren Container. Nicht beschriftete zugehörige Werte erhalten generierte Schlüssel wie _0. Dadurch erhält ein String-Fall strukturelle Wrapper, statt zu einem einfachen String zu werden. Diese Regeln sind unter Codable synthesis for enums with associated values dokumentiert.

Mit CodingKeys lassen sich diese Wrapper umbenennen, doch bei beibehaltener Synthese bleibt auch ihr Strukturmodell bestehen. Die unterstützte Lösung besteht darin, init(from:) und encode(to:) selbst zu implementieren. Apple dokumentiert die manuelle Konformität für Typen, deren Swift-Struktur sich von ihrer kodierten Struktur unterscheidet, ausdrücklich unter Encoding and Decoding Custom Types.

Rekursion und Darstellung sind getrennte Entscheidungen. Swifts Modifizierer indirect ermöglicht rekursive Enum-Speicherung; eigene Kodierungsmethoden bestimmen die kodierte Form. Der Sprachleitfaden erklärt unter Enumerations, dass indirect vor die Enum-Deklaration gesetzt wird, um Indirektion auf die zugehörigen Werte der Fälle anzuwenden.

Was genau soll die einfache Darstellung erhalten?

Verwende für dieses Beispiel den folgenden Vertrag. Er unterstützt bewusst nur ein kleines Wertvokabular, das sich für eine synthetische Layoutvorschau eignet. Die dokumentierten Konvertierungsregeln von Foundation erlauben Strings, Zahlen, Arrays, Dictionaries und Null-Objekte, verlangen String-Schlüssel für Dictionaries und schließen nicht endliche Zahlen aus. Siehe JSONSerialization.

Enum-Fall Einfacher Wert Kompatibilitätsanforderung
null NSNull Einen expliziten Nullwert erhalten
bool Boolescher Wert Von numerischer null und eins unterscheidbar halten
number Endlicher Double-Wert Die Präzisionsgrenzen von Double akzeptieren
string String Text ohne numerische Konvertierung erhalten
array Array einfacher Werte Elementreihenfolge und leere Arrays erhalten
object Dictionary mit String-Schlüsseln Schlüssel, verschachtelte Werte und leere Objekte erhalten

Kompatibilität bedeutet hier, diese Kategorien und ihre Werte innerhalb der angegebenen numerischen Grenzen zu erhalten. Die konkrete Foundation-Zahlenunterklasse oder die Reihenfolge der Dictionary-Schlüssel muss dabei nicht erhalten bleiben. Auch ein expliziter Null-Eintrag muss erhalten bleiben: Ein Objekt mit "subtitle": NSNull() hat eine andere Modellbedeutung als ein Objekt ohne den Schlüssel subtitle.

Entscheide vor der Übernahme dieses Vertrags, ob Zahlen tatsächlich alle denselben Double-Fall verwenden können. Apple beschreibt Double als 64-Bit-Gleitkommatyp und bietet Initialisierer für exakte Konvertierungen an, da gewöhnliche Konvertierungen Rundungsfehler verursachen können. Wenn dein Modell exakte große Ganzzahlen benötigt, lege vor der Konvertierung eine ausdrückliche Zahlenrichtlinie fest. Ein zusätzlicher Integer-Fall erfordert auch eine Dekodierungsregel, da einfache Zahlenwerte keine Swift-Enum-Fallidentität mitführen. Siehe Double.

Wie implementierst du das rekursive Enum?

Verwende eine aktuelle Mac-Swift-Umgebung mit verfügbarer Foundation-Bibliothek. Die nächsten drei Codeblöcke ergeben in dieser Reihenfolge ein vollständiges Top-Level-Beispiel: das Enum, Konvertierungsfunktionen und eine ausführbare Prüfroutine. Alle Beispielwerte werden im Arbeitsspeicher erstellt. Swift ist in der Rechercheumgebung nicht verfügbar, daher wurde das Beispiel geprüft, aber nicht ausgeführt.

Der Decoder prüft zuerst auf null und versucht anschließend, die unterstützten primitiven Typen zu dekodieren. Apples SingleValueDecodingContainer stellt diese Operationen bereit, einschließlich decodeNil(). Der lokale Hilfsmechanismus behandelt nur einen Typfehler als Anlass, einen weiteren primitiven Typ zu versuchen; andere Dekodierungsfehler werden weitergegeben.

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

Jeder Kodierungszweig schreibt seine Nutzlast direkt. Der Null-Zweig ruft ausdrücklich encodeNil() auf, und die Sammlungszweige überlassen die Kodierung den Implementierungen von Arrays und Dictionaries. Apple dokumentiert sowohl primitive als auch generische Kodierungsoperationen unter SingleValueEncodingContainer und die Konformität von Sammlungen unter Encoding and Decoding Custom Types.

Die Erkennung von Arrays ist bewusst von der Dekodierung ihrer Elemente getrennt. Das Anfordern eines Unkeyed Containers kann einen Typfehler auslösen, wenn der aktuelle Wert kein Array ist. Sobald ein Array erkannt wurde, wird jedes Element mit gewöhnlichem try dekodiert. Ein Fehler bei einem Element wird daher weitergegeben, statt einen sachfremden Versuch der Objektdekodierung auszulösen. Das dokumentierte Verhalten wird unter unkeyedContainer() und UnkeyedDecodingContainer beschrieben.

Diese Implementierung ist für den JSON-Kodierungspfad von Foundation und das angegebene Wertvokabular ausgelegt. Prüfe jeden anderen Encoder oder Decoder anhand desselben Vertrags, bevor du ihn verwendest; die Konformität mit Codable allein gewährleistet kein identisches Verhalten in verschiedenen Darstellungsformaten.

Wie wandelst du Werte vollständig im Arbeitsspeicher in Foundation-Objekte um?

Foundation bietet zwei nützliche Schnittstellen: JSONEncoder erzeugt aus einem kodierbaren Wert Data, und JSONDecoder stellt aus Data einen dekodierbaren Wert wieder her. JSONSerialization konvertiert zwischen dieser Zwischendarstellung und Foundation-Objekten. Die Kombination dieser dokumentierten Operationen ergibt die folgende In-Memory-Brücke. Siehe JSONEncoder, JSONDecoder und 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)
}

Die Zwischenform Data bleibt im Arbeitsspeicher. Diese Funktionen führen Serialisierungsarbeit aus; sie sind keine kopierfreien Casts zwischen dem Enum und Foundation-Sammlungen. Führe die Konvertierung an der Stelle durch, an der dein Desktop-Feature tatsächlich einen einfachen Objektgraphen benötigt, und behalte das Enum für typisierte Modelloperationen bei.

Beide Optionen .fragmentsAllowed sind wichtig, da ein rekursiver Wert als Skalar an der Wurzel stehen kann. Ein String, Boolescher Wert, eine Zahl oder null an der obersten Ebene muss genauso behandelt werden wie ein verschachtelter Wert. Foundation dokumentiert dieses Verhalten separat für ReadingOptions.fragmentsAllowed und WritingOptions.fragmentsAllowed.

Die Rückkonvertierungsfunktion akzeptiert Any, da dies der Eingabetyp der Foundation-API ist. Der unterstützte Wertebereich bleibt jedoch durch den obigen Vertrag begrenzt. Konvertierungsfehler werden an den Aufrufer weitergegeben. Ersetze eine fehlgeschlagene Konvertierung nicht durch ein leeres Objekt und entferne keine Elemente, deren Konvertierung fehlschlägt; das würde das Modell unbemerkt verändern.

Welche Prüfungen belegen die Kompatibilität?

Eine einfache Hin- und Rückkonvertierung kann das ursprüngliche Problem übersehen: Synthetisierte Enum-Kodierung kann ihre eigene umhüllte Struktur erfolgreich hin- und zurückkonvertieren. Prüfe sowohl die Enum-Gleichheit als auch die einfache Form. Die folgende Routine erstellt unabhängig davon einen Foundation-Objektgraphen, überprüft das erwartete Enum-Modell, kontrolliert die tatsächlichen Schlüssel der Ausgabe an der Wurzel und testet jede Wurzelkategorie.

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

Die erwartete Erfolgsmeldung lautet All compatibility checks passed. Die Prüfung der Wurzelschlüssel erkennt einen hinzugefügten äußeren Fall-Wrapper. Das unabhängig erstellte erwartete Modell erkennt eine falsche Fallauswahl, auch bei Boolescher Wert gegenüber Zahl und numerisch aussehendem Text gegenüber Zahl. Die Null-Prüfung bestätigt, dass subtitle vorhanden bleibt, während die Wurzelprüfungen skalare Fragmente abdecken und leere Arrays von leeren Objekten unterscheiden.

Passe diese Diagnosesequenz für ein bestehendes Desktop-Modell an seine synthetischen Werte an: Prüfe zuerst das erwartete Enum, untersuche dann die einfache Form, stelle anschließend das Enum wieder her und teste zuletzt Grenzfälle. Benenne Zwischenergebnisse, damit eine fehlgeschlagene Prüfung erkennen lässt, bei welcher Konvertierung sich der Wert geändert hat.

Wo liegen die Grenzen dieses Ansatzes?

Der Zahlenvertrag ist die wichtigste Einschränkung. Ein einzelner Double-Fall kann nicht die exakte Erhaltung jedes numerischen Foundation-Eingabewerts garantieren. Wenn eine exakte Ganzzahlkonvertierung wichtig ist, prüfe die Darstellbarkeit, bevor du den Wert in dieses Modell überführst, oder gestalte die Zahlenfälle und ihre Dekodierungsregeln neu. Eine erfolgreiche Serialisierung belegt keine exakte Erhaltung numerischer Werte. Apples Double documentation unterscheidet zwischen gerundeten und exakten Konvertierungen.

Das Beispiel weist nicht endliche Zahlen außerdem explizit in beiden Kodierungsrichtungen zurück. Damit bleibt die Grenze des Enums an die dokumentierten Einschränkungen der einfachen Foundation-Darstellung angepasst. Eine rekursive Struktur bedeutet keine unbegrenzte Verschachtelungstiefe. Halte daher die Bäume des synthetischen Modells angemessen klein und gib Konvertierungsfehler an das aufrufende Feature weiter.

Für die Aufgabe mit der Layoutvorschau ist das fertige Ergebnis ein typisiertes rekursives Enum, dessen eigene Kodierung einfache Strings, Boolesche Werte, endliche Zahlen, Nullwerte, Arrays und Objekte mit String-Schlüsseln erhält. Verwende Formprüfungen zusammen mit Hin- und Rückkonvertierungsprüfungen, wenn du Fälle oder Konvertierungslogik änderst. So wird die beabsichtigte Darstellung konkret und überprüfbar.

Quellen