Para preservar valores aninhados simples com um enum recursivo Swift, implemente codificação e decodificação personalizadas para cada tipo de valor. Este passo a passo constrói um modelo sintético de app Mac que faz round-trip através de objetos Foundation inteiramente em memória, sem adicionar wrappers de caso do enum.
Por que a síntese do Codable altera a representação?
Suponha que um app Mac de desktop tenha uma pequena prévia de layout contendo rótulos, valores numéricos, switches, listas e grupos aninhados. Um enum recursivo pode descrever esses valores com precisão. A tarefa concreta aqui é converter esse enum em um grafo de objetos Foundation simples e reconstruí-lo preservando os tipos de valor suportados.
A conformância automática não fornece a representação necessária. O design de síntese de enum implementado pelo Swift coloca o nome do caso em um container com chave externo, depois coloca os valores associados dentro de outro container. Valores associados sem rótulo recebem chaves geradas como _0. Consequentemente, um caso de string adquire wrappers estruturais em vez de se tornar uma string simples. Essas regras são documentadas em Síntese Codable para enums com valores associados.
Alterar CodingKeys pode renomear esses wrappers, mas manter a síntese mantém seu modelo estrutural. A solução suportada é implementar init(from:) e encode(to:) você mesmo. A Apple documenta explicitamente a conformância manual para tipos cuja estrutura Swift difere de sua estrutura codificada em Codificação e Decodificação de Tipos Personalizados.
Recursão e representação são decisões separadas. O modificador indirect do Swift permite armazenamento de enum recursivo; métodos de codificação personalizados determinam a forma codificada. O guia da linguagem explica colocar indirect antes da declaração do enum para aplicar indireção aos seus casos de valor associado em Enumerações.
O que exatamente a representação simples deve preservar?
Use o seguinte contrato para este exemplo. Ele suporta deliberadamente um pequeno vocabulário de valor adequado para uma prévia de layout sintética. As regras de conversão documentadas do Foundation permitem strings, números, arrays, dicionários e objetos nulos, exigem chaves de dicionário do tipo string e excluem números não-finitos. Veja JSONSerialization.
| Caso do enum | Valor simples | Requisito de compatibilidade |
|---|---|---|
null |
NSNull |
Preservar um valor nulo explícito |
bool |
Valor booleano | Manter distinto de zero e um numéricos |
number |
Valor Double finito |
Aceitar os limites de precisão do Double |
string |
String | Preservar texto sem conversão numérica |
array |
Array de valores simples | Preservar ordem dos elementos e arrays vazios |
object |
Dicionário com chaves string | Preservar chaves, valores aninhados e objetos vazios |
Compatibilidade aqui significa preservar essas categorias e seus valores dentro dos limites numéricos declarados. Não exige preservar uma subclasse particular de número Foundation ou a ordem em que as chaves do dicionário aparecem. Uma entrada nula explícita também deve sobreviver: um objeto contendo "subtitle": NSNull() tem um significado de modelo diferente de um objeto sem a chave subtitle.
Antes de adotar este contrato, decida se números realmente podem compartilhar um único caso Double. A Apple documenta Double como um tipo de ponto flutuante de 64 bits e fornece inicializadores de conversão exata porque conversões ordinárias podem arredondar. Se seu modelo precisa de inteiros grandes exatos, escolha uma política numérica explícita antes de convertê-los. Adicionar um caso inteiro também requer uma regra de decodificação, porque valores numéricos simples não carregam identidades de caso do enum Swift. Veja Double.
Como implementar o enum recursivo?
Use um ambiente Mac Swift atual com Foundation disponível. Os próximos três blocos de código, em ordem, formam um exemplo completo de nível superior: o enum, funções de conversão e uma rotina de verificação executável. Todos os valores de exemplo são criados em memória. O Swift não está disponível no ambiente de pesquisa, então o exemplo foi inspecionado mas não executado.
O decodificador primeiro verifica nulo, depois tenta os tipos primitivos suportados. O SingleValueDecodingContainer da Apple fornece essas operações, incluindo decodeNil(). O helper local trata apenas incompatibilidade de tipo como razão para tentar outro primitivo; outros erros de decodificação propagam.
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)
}
}
}
Cada branch de codificação escreve seu payload diretamente. O branch nulo chama explicitamente encodeNil(), e os branches de coleção delegam às implementações de codificação de arrays e dicionários. A Apple documenta operações de codificação primitivas e genéricas em SingleValueEncodingContainer, e conformância de coleção em Codificação e Decodificação de Tipos Personalizados.
A detecção de array é deliberadamente separada da decodificação de elementos do array. Solicitar um container sem chave pode lançar incompatibilidade de tipo quando o valor atual não é um array. Uma vez reconhecido um array, cada elemento é decodificado com try comum, então uma falha de elemento escapa em vez de desencadear uma tentativa de decodificação de objeto não relacionada. O comportamento documentado é descrito em unkeyedContainer() e UnkeyedDecodingContainer.
Esta implementação visa o caminho de codificação JSON do Foundation e o vocabulário de valor declarado. Teste qualquer outro encoder ou decoder contra o mesmo contrato antes de usá-lo; conformância com Codable sozinha não estabelece comportamento idêntico entre formatos de representação.
Como converter para objetos Foundation inteiramente em memória?
O Foundation expõe duas fronteiras úteis: JSONEncoder produz Data a partir de um valor encodable, e JSONDecoder reconstrói um valor decodable a partir de Data. JSONSerialization converte entre essa representação intermediária e objetos Foundation. Combinar essas operações documentadas fornece a seguinte ponte em memória. Veja 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)
}
O Data intermediário permanece em memória. Estas funções realizam trabalho de serialização; não são casts zero-copy entre o enum e coleções Foundation. Mantenha a conversão na fronteira onde seu recurso de desktop realmente precisa de um grafo de objetos simples, enquanto retém o enum para operações de modelo tipado.
Ambas as opções .fragmentsAllowed importam porque um valor recursivo pode ter uma raiz escalar. Uma string, booleano, número ou nulo de nível superior precisa do mesmo tratamento que um valor aninhado. O Foundation documenta este comportamento separadamente para ReadingOptions.fragmentsAllowed e WritingOptions.fragmentsAllowed.
A função reversa aceita Any porque esse é o tipo de entrada da API do Foundation, mas seu domínio suportado permanece o contrato acima. Erros de conversão propagam para o chamador. Não substitua uma conversão mal-sucedida por um objeto vazio nem remova elementos que falharam ao converter; fazer isso alteraria o modelo silenciosamente.
Quais verificações demonstram compatibilidade?
Um round-trip sozinho pode perder o problema original: a codificação de enum sintetizada pode fazer round-trip de sua própria estrutura wrappada com sucesso. Verifique tanto a igualdade do enum quanto a forma simples. A rotina a seguir constrói um grafo Foundation independente, verifica seu modelo enum esperado, checa as chaves raiz reais da saída e exercita cada categoria raiz.
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()
A mensagem de sucesso esperada é All compatibility checks passed. A verificação de chave raiz detecta um wrapper de caso externo adicionado. O modelo esperado independente detecta seleção de caso errada, incluindo booleano versus número e texto com aparência numérica versus número. A verificação de nulo confirma que subtitle permanece presente, enquanto as verificações de raiz cobrem fragmentos escalares e distinguem arrays vazios de objetos vazios.
Para um modelo de desktop existente, adapte esta sequência de diagnóstico aos seus valores sintéticos: primeiro verifique o enum esperado, depois inspecione a forma simples, depois reconstrua o enum e finalmente exercite casos de fronteira. Mantenha resultados intermediários nomeados para que uma verificação falhada identifique qual conversão alterou o valor.
Quais são os limites desta abordagem?
O contrato numérico é a principal limitação. Um único caso Double não pode prometer preservação exata de toda entrada numérica Foundation. Onde conversão exata de inteiro importa, valide representabilidade antes de entrar neste modelo ou redesenhe os casos numéricos e suas regras de decodificação. Serialização bem-sucedida não é evidência de preservação numérica exata. A documentação do Double da Apple distingue conversões arredondadas de conversões exatas.
O exemplo também rejeita números não-finitos explicitamente em ambas as direções de codificação. Isso mantém a fronteira do enum alinhada com as restrições documentadas da representação Foundation simples. Estrutura recursiva não implica capacidade de aninhamento ilimitada, então mantenha árvores de modelo sintético razoavelmente dimensionadas e deixe erros de conversão alcançarem o recurso chamador.
Para a tarefa de prévia de layout, o resultado final é um enum recursivo tipado cuja codificação personalizada preserva strings simples, booleanos, números finitos, nulos, arrays e objetos com chaves string. Use as verificações de forma junto com verificações de round-trip sempre que alterar os casos ou lógica de conversão; juntas elas tornam a representação pretendida concreta e revisável.
Fontes
- Síntese Codable para enums com valores associados — Projeto Swift.
- Enumerações — Projeto Swift.
- Codificação e Decodificação de Tipos Personalizados — Apple.
- SingleValueDecodingContainer — Apple.
- SingleValueEncodingContainer — Apple.
- unkeyedContainer() — Apple.
- UnkeyedDecodingContainer — Apple.
- JSONEncoder — Apple.
- JSONDecoder — Apple.
- JSONSerialization — Apple.
- ReadingOptions.fragmentsAllowed — Apple.
- WritingOptions.fragmentsAllowed — Apple.
- Double — Apple.