Pour préserver les valeurs imbriquées simples avec une énumération récursive Swift, implémentez un encodage et un décodage personnalisés pour chaque type de valeur. Ce guide construit un modèle synthétique d’application Mac qui effectue un aller-retour via des objets Foundation entièrement en mémoire, sans ajouter d’enveloppes pour les cas de l’énumération.
Pourquoi Codable synthétisé modifie-t-il la représentation ?
Supposons qu’une application de bureau Mac comporte un petit aperçu de mise en page contenant des libellés, des valeurs numériques, des interrupteurs, des listes et des groupes imbriqués. Une énumération récursive peut décrire précisément ces valeurs. La tâche concrète consiste ici à convertir cette énumération en un graphe d’objets Foundation simple et à le reconstruire en préservant les types de valeurs pris en charge.
La conformité automatique ne fournit pas la représentation requise. La conception de synthèse des énumérations implémentée par Swift place le nom du cas dans un conteneur à clés externe, puis place les valeurs associées dans un autre conteneur. Les valeurs associées sans étiquette reçoivent des clés générées telles que _0. Le cas chaîne acquiert donc des enveloppes structurelles au lieu de devenir une chaîne simple. Ces règles sont documentées dans Codable synthesis for enums with associated values.
Modifier CodingKeys peut renommer ces enveloppes, mais conserver la synthèse revient à conserver son modèle structurel. La solution prise en charge consiste à implémenter vous-même init(from:) et encode(to:). Apple documente explicitement la conformité manuelle pour les types dont la structure Swift diffère de leur structure encodée dans Encoding and Decoding Custom Types.
La récursivité et la représentation sont deux décisions distinctes. Le modificateur Swift indirect permet le stockage d’énumérations récursives ; les méthodes de codage personnalisées déterminent la forme encodée. Le guide du langage explique qu’il faut placer indirect avant la déclaration de l’énumération pour appliquer l’indirection à ses cas à valeurs associées dans Enumerations.
Que doit préserver exactement la représentation simple ?
Utilisez le contrat suivant pour cet exemple. Il prend volontairement en charge un petit vocabulaire de valeurs adapté à un aperçu synthétique de mise en page. Les règles de conversion documentées de Foundation autorisent les chaînes, les nombres, les tableaux, les dictionnaires et les objets null, exigent des clés de dictionnaire de type chaîne et excluent les nombres non finis. Voir JSONSerialization.
| Cas de l’énumération | Valeur simple | Exigence de compatibilité |
|---|---|---|
null |
NSNull |
Préserver une valeur null explicite |
bool |
Valeur booléenne | La distinguer de zéro et de un numériques |
number |
Valeur Double finie |
Accepter les limites de précision de Double |
string |
Chaîne | Préserver le texte sans conversion numérique |
array |
Tableau de valeurs simples | Préserver l’ordre des éléments et les tableaux vides |
object |
Dictionnaire à clés de type chaîne | Préserver les clés, les valeurs imbriquées et les objets vides |
La compatibilité signifie ici préserver ces catégories et leurs valeurs dans les limites numériques indiquées. Elle n’exige pas de préserver une sous-classe Foundation particulière pour les nombres ni l’ordre d’apparition des clés du dictionnaire. Une entrée null explicite doit également être conservée : un objet contenant "subtitle": NSNull() a une signification différente dans le modèle d’un objet sans clé subtitle.
Avant d’adopter ce contrat, déterminez si les nombres peuvent réellement partager un seul cas Double. Apple décrit Double comme un type à virgule flottante de 64 bits et fournit des initialiseurs de conversion exacte, car les conversions ordinaires peuvent arrondir. Si votre modèle doit prendre en charge les grands entiers exacts, choisissez une politique numérique explicite avant de les convertir. L’ajout d’un cas entier exige aussi une règle de décodage, car les valeurs numériques simples ne portent pas l’identité de leur cas d’énumération Swift. Voir Double.
Comment implémenter l’énumération récursive ?
Utilisez un environnement Swift Mac récent avec Foundation disponible. Les trois blocs de code suivants, dans l’ordre, constituent un exemple complet de niveau supérieur : l’énumération, les fonctions de conversion et une routine de vérification exécutable. Toutes les valeurs d’exemple sont créées en mémoire. Swift n’étant pas disponible dans l’environnement de recherche, l’exemple a été examiné, mais pas exécuté.
Le décodeur vérifie d’abord la valeur null, puis tente les types primitifs pris en charge. Le SingleValueDecodingContainer d’Apple fournit ces opérations, notamment decodeNil(). L’assistant local ne traite qu’une incompatibilité de type comme un motif pour essayer un autre type primitif ; les autres erreurs de décodage sont propagées.
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)
}
}
}
Chaque branche d’encodage écrit directement sa charge utile. La branche null appelle explicitement encodeNil(), et les branches des collections délèguent aux implémentations de codage des tableaux et des dictionnaires. Apple documente les opérations d’encodage primitives et génériques dans SingleValueEncodingContainer, ainsi que la conformité des collections dans Encoding and Decoding Custom Types.
La détection des tableaux est délibérément séparée du décodage de leurs éléments. La demande d’un conteneur non indexé peut déclencher une incompatibilité de type si la valeur actuelle n’est pas un tableau. Une fois le tableau reconnu, chaque élément est décodé avec try ordinaire ; l’échec d’un élément est donc propagé au lieu de déclencher une tentative sans rapport de décodage d’objet. Le comportement documenté est décrit dans unkeyedContainer() et UnkeyedDecodingContainer.
Cette implémentation cible le chemin de codage JSON de Foundation et le vocabulaire de valeurs indiqué. Testez tout autre encodeur ou décodeur selon le même contrat avant de l’utiliser ; la seule conformité à Codable ne garantit pas un comportement identique entre les formats de représentation.
Comment convertir des objets Foundation entièrement en mémoire ?
Foundation expose deux interfaces utiles : JSONEncoder produit des Data à partir d’une valeur encodable, et JSONDecoder reconstruit une valeur décodable à partir de Data. JSONSerialization convertit cette représentation intermédiaire en objets Foundation, et inversement. La combinaison de ces opérations documentées fournit le pont en mémoire suivant. Voir JSONEncoder, JSONDecoder et 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)
}
Les Data intermédiaires restent en mémoire. Ces fonctions effectuent un travail de sérialisation ; elles ne réalisent pas de conversion sans copie entre l’énumération et les collections Foundation. Effectuez la conversion à la frontière où votre fonctionnalité de bureau a réellement besoin d’un graphe d’objets simples, tout en conservant l’énumération pour les opérations sur le modèle typé.
Les deux options .fragmentsAllowed sont importantes, car une valeur récursive peut avoir une valeur scalaire à la racine. Une chaîne, une valeur booléenne, un nombre ou une valeur null au niveau supérieur doit être traité comme une valeur imbriquée. Foundation documente ce comportement séparément pour ReadingOptions.fragmentsAllowed et WritingOptions.fragmentsAllowed.
La fonction inverse accepte Any parce que c’est le type d’entrée de l’API Foundation, mais son domaine pris en charge reste celui du contrat ci-dessus. Les erreurs de conversion sont propagées à l’appelant. Ne remplacez pas une conversion infructueuse par un objet vide et ne supprimez pas les éléments qui ne peuvent pas être convertis ; cela modifierait silencieusement le modèle.
Quelles vérifications démontrent la compatibilité ?
Un simple aller-retour peut passer à côté du problème initial : le codage synthétisé d’une énumération peut réussir l’aller-retour de sa propre structure enveloppée. Vérifiez à la fois l’égalité de l’énumération et la forme simple. La routine suivante construit un graphe Foundation indépendant, vérifie le modèle attendu de l’énumération, contrôle les clés réelles de l’objet racine et teste chaque catégorie de racine.
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()
Le message de réussite attendu est All compatibility checks passed. La vérification des clés de la racine détecte l’ajout d’une enveloppe externe pour le cas. Le modèle attendu indépendant détecte un mauvais choix de cas, notamment entre booléen et nombre, ou entre texte ressemblant à un nombre et nombre. La vérification de la valeur null confirme que subtitle reste présent, tandis que les vérifications de la racine couvrent les fragments scalaires et distinguent les tableaux vides des objets vides.
Pour un modèle de bureau existant, adaptez cette séquence de diagnostic à ses valeurs synthétiques : vérifiez d’abord l’énumération attendue, inspectez ensuite la forme simple, reconstruisez l’énumération, puis testez les cas limites. Donnez des noms aux résultats intermédiaires afin qu’une vérification en échec indique quelle conversion a modifié la valeur.
Quelles sont les limites de cette approche ?
Le contrat numérique constitue la principale limite. Un seul cas Double ne peut pas garantir la préservation exacte de toutes les valeurs numériques Foundation en entrée. Lorsqu’une conversion entière exacte est importante, validez la représentabilité avant d’intégrer ces valeurs à ce modèle, ou repensez les cas numériques et leurs règles de décodage. Une sérialisation réussie ne prouve pas que les valeurs numériques ont été préservées exactement. La documentation d’Apple sur Double distingue les conversions arrondies des conversions exactes.
L’exemple rejette aussi explicitement les nombres non finis dans les deux sens de codage. L’énumération reste ainsi alignée sur les restrictions documentées de la représentation Foundation simple. Une structure récursive n’implique pas une capacité d’imbrication illimitée ; gardez donc les arbres de modèles synthétiques de taille raisonnable et laissez les erreurs de conversion remonter jusqu’à la fonctionnalité appelante.
Pour la tâche d’aperçu de mise en page, le résultat final est une énumération récursive typée dont le codage personnalisé préserve les chaînes simples, les booléens, les nombres finis, les valeurs null, les tableaux et les objets à clés de type chaîne. Utilisez les vérifications de forme en complément des vérifications d’aller-retour chaque fois que vous modifiez les cas ou la logique de conversion ; ensemble, elles rendent la représentation voulue concrète et vérifiable.
Sources
- 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.