Untuk mempertahankan nilai bersarang biasa dengan enum Swift rekursif, implementasikan pengodean dan penguraian kode khusus untuk setiap jenis nilai. Panduan ini membuat model app Mac sintetis yang melakukan perjalanan pulang-pergi melalui objek Foundation sepenuhnya di memori, tanpa menambahkan pembungkus kasus enum.
Mengapa Codable hasil sintesis mengubah representasi?
Misalkan sebuah app desktop Mac memiliki pratinjau tata letak kecil yang berisi label, nilai numerik, sakelar, daftar, dan grup bersarang. Enum rekursif dapat mendeskripsikan nilai-nilai tersebut secara tepat. Tugas konkretnya di sini adalah mengonversi enum tersebut menjadi graf objek Foundation biasa dan merekonstruksinya sambil mempertahankan jenis nilai yang didukung.
Konformansi otomatis tidak menyediakan representasi yang dibutuhkan. Rancangan sintesis enum Swift yang diterapkan menempatkan nama kasus dalam kontainer berkunci di bagian luar, lalu menempatkan nilai terkait di dalam kontainer lain. Nilai terkait tanpa label mendapatkan kunci yang dibuat secara otomatis, seperti _0. Akibatnya, kasus string memperoleh pembungkus struktural alih-alih menjadi string biasa. Aturan ini didokumentasikan dalam Codable synthesis for enums with associated values.
Mengubah CodingKeys dapat mengganti nama pembungkus tersebut, tetapi mempertahankan sintesis berarti mempertahankan model strukturalnya. Solusi yang didukung adalah mengimplementasikan sendiri init(from:) dan encode(to:). Apple secara eksplisit mendokumentasikan konformansi manual untuk tipe yang struktur Swift-nya berbeda dari struktur hasil pengodeannya dalam Encoding and Decoding Custom Types.
Rekursi dan representasi adalah keputusan yang terpisah. Modifier indirect di Swift memungkinkan penyimpanan enum rekursif; metode pengodean khusus menentukan bentuk hasil pengodean. Panduan bahasa menjelaskan penempatan indirect sebelum deklarasi enum untuk menerapkan indirection pada kasus nilai terkaitnya dalam Enumerations.
Apa tepatnya yang harus dipertahankan oleh representasi biasa?
Gunakan kontrak berikut untuk contoh ini. Kontrak ini sengaja mendukung kosakata nilai yang kecil dan cocok untuk pratinjau tata letak sintetis. Aturan konversi Foundation yang terdokumentasi mengizinkan string, angka, array, kamus, dan objek null, mewajibkan kunci kamus berupa string, serta mengecualikan angka non-finit. Lihat JSONSerialization.
| Kasus enum | Nilai biasa | Persyaratan kompatibilitas |
|---|---|---|
null |
NSNull |
Pertahankan nilai null eksplisit |
bool |
Nilai Boolean | Bedakan dari nol dan satu numerik |
number |
Nilai Double finit |
Terima batas presisi Double |
string |
String | Pertahankan teks tanpa konversi numerik |
array |
Array nilai biasa | Pertahankan urutan elemen dan array kosong |
object |
Kamus berkunci string | Pertahankan kunci, nilai bersarang, dan objek kosong |
Kompatibilitas di sini berarti mempertahankan kategori dan nilainya dalam batas numerik yang dinyatakan. Kompatibilitas tidak mengharuskan dipertahankannya subkelas angka Foundation tertentu atau urutan kemunculan kunci kamus. Entri null eksplisit juga harus tetap ada: objek yang berisi "subtitle": NSNull() memiliki makna model yang berbeda dari objek tanpa kunci subtitle.
Sebelum menerapkan kontrak ini, tentukan apakah angka memang dapat menggunakan satu kasus Double. Apple mendokumentasikan Double sebagai tipe titik mengambang 64-bit dan menyediakan inisialisator konversi eksak karena konversi biasa dapat membulatkan nilai. Jika model Anda memerlukan bilangan bulat besar yang eksak, pilih kebijakan numerik yang jelas sebelum mengonversinya. Menambahkan kasus bilangan bulat juga memerlukan aturan penguraian kode, karena nilai numerik biasa tidak membawa identitas kasus enum Swift. Lihat Double.
Bagaimana cara mengimplementasikan enum rekursif?
Gunakan lingkungan Swift Mac terkini dengan Foundation tersedia. Tiga blok kode berikut, secara berurutan, membentuk satu contoh tingkat atas yang lengkap: enum, fungsi konversi, dan rutinitas pemeriksaan yang dapat dijalankan. Semua nilai contoh dibuat di memori. Swift tidak tersedia di lingkungan riset, jadi contoh ini telah ditinjau tetapi tidak dijalankan.
Dekoder terlebih dahulu memeriksa null, lalu mencoba tipe primitif yang didukung. SingleValueDecodingContainer dari Apple menyediakan operasi-operasi ini, termasuk decodeNil(). Helper lokal hanya menganggap ketidakcocokan tipe sebagai alasan untuk mencoba primitif lain; kesalahan penguraian kode lainnya diteruskan.
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)
}
}
}
Setiap cabang pengodean menulis muatannya secara langsung. Cabang null secara eksplisit memanggil encodeNil(), sedangkan cabang koleksi meneruskan pengodean kepada implementasi array dan kamus. Apple mendokumentasikan operasi pengodean primitif dan generik dalam SingleValueEncodingContainer, serta konformansi koleksi dalam Encoding and Decoding Custom Types.
Deteksi array sengaja dipisahkan dari penguraian kode elemen array. Meminta kontainer tanpa kunci dapat menimbulkan ketidakcocokan tipe ketika nilai saat ini bukan array. Setelah sebuah array dikenali, setiap elemen diuraikan dengan try biasa, sehingga kegagalan pada elemen diteruskan alih-alih memicu upaya penguraian objek yang tidak terkait. Perilaku yang didokumentasikan dijelaskan dalam unkeyedContainer() dan UnkeyedDecodingContainer.
Implementasi ini ditujukan untuk jalur pengodean JSON Foundation dan kosakata nilai yang telah dinyatakan. Uji encoder atau dekoder lain terhadap kontrak yang sama sebelum menggunakannya; konformansi terhadap Codable saja tidak menjamin perilaku yang identik di berbagai format representasi.
Bagaimana cara mengonversi ke objek Foundation sepenuhnya di memori?
Foundation menyediakan dua batas yang berguna: JSONEncoder menghasilkan Data dari nilai yang dapat dikodekan, dan JSONDecoder merekonstruksi nilai yang dapat diuraikan dari Data. JSONSerialization mengonversi antara representasi perantara tersebut dan objek Foundation. Menggabungkan operasi yang terdokumentasi ini menghasilkan jembatan dalam memori berikut. Lihat JSONEncoder, JSONDecoder, dan 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)
}
Data perantara tetap berada di memori. Fungsi-fungsi ini melakukan pekerjaan serialisasi; fungsi tersebut bukan cast tanpa salinan antara enum dan koleksi Foundation. Tempatkan konversi pada batas tempat fitur desktop Anda benar-benar memerlukan graf objek biasa, sambil tetap menggunakan enum untuk operasi model bertipe.
Kedua opsi .fragmentsAllowed penting karena nilai rekursif dapat memiliki akar skalar. String, Boolean, angka, atau null tingkat atas memerlukan perlakuan yang sama seperti nilai bersarang. Foundation mendokumentasikan perilaku ini secara terpisah untuk ReadingOptions.fragmentsAllowed dan WritingOptions.fragmentsAllowed.
Fungsi kebalikannya menerima Any karena itulah tipe masukan API Foundation, tetapi domain yang didukung tetap mengikuti kontrak di atas. Kesalahan konversi diteruskan kepada pemanggil. Jangan mengganti konversi yang gagal dengan objek kosong atau menghapus elemen yang gagal dikonversi; tindakan tersebut akan mengubah model secara diam-diam.
Pemeriksaan apa yang menunjukkan kompatibilitas?
Uji perjalanan pulang-pergi saja dapat melewatkan masalah awal: pengodean enum hasil sintesis dapat berhasil melakukan perjalanan pulang-pergi dengan struktur terbungkusnya sendiri. Periksa kesetaraan enum dan bentuk nilai biasa. Rutinitas berikut membuat graf Foundation yang terpisah, memverifikasi model enum yang diharapkan, memeriksa kunci akar keluaran yang sebenarnya, dan menguji setiap kategori akar.
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()
Pesan keberhasilan yang diharapkan adalah All compatibility checks passed. Pemeriksaan kunci akar mendeteksi pembungkus kasus luar yang ditambahkan. Model ekspektasi yang terpisah mendeteksi pemilihan kasus yang salah, termasuk Boolean versus angka dan teks yang menyerupai angka versus angka. Pemeriksaan null memastikan subtitle tetap ada, sementara pemeriksaan akar mencakup fragmen skalar dan membedakan array kosong dari objek kosong.
Untuk model desktop yang sudah ada, sesuaikan urutan diagnostik ini dengan nilai sintetisnya: pertama verifikasi enum yang diharapkan, lalu periksa bentuk nilai biasa, kemudian rekonstruksi enum, dan terakhir uji kasus batas. Beri nama pada hasil perantara agar pemeriksaan yang gagal menunjukkan konversi mana yang mengubah nilai.
Apa batasan pendekatan ini?
Kontrak numerik adalah batasan utamanya. Satu kasus Double tidak dapat menjanjikan pelestarian eksak untuk setiap masukan numerik Foundation. Jika konversi bilangan bulat eksak penting, validasi keterwakilan sebelum memasukkan nilai ke model ini atau rancang ulang kasus numerik beserta aturan penguraian kodenya. Serialisasi yang berhasil bukan bukti bahwa nilai numerik dipertahankan secara eksak. Dokumentasi Double dari Apple membedakan konversi yang membulatkan dari konversi eksak.
Contoh ini juga secara eksplisit menolak angka nonfinit dalam kedua arah pengodean. Dengan demikian, batas enum tetap selaras dengan batasan terdokumentasi pada representasi Foundation biasa. Struktur rekursif tidak berarti kapasitas bersarangnya tanpa batas, jadi jaga agar pohon model sintetis tetap berukuran wajar dan biarkan kesalahan konversi diteruskan ke fitur pemanggil.
Untuk tugas pratinjau tata letak, hasil akhirnya adalah enum rekursif bertipe yang pengodean khususnya mempertahankan string biasa, Boolean, angka finit, null, array, dan objek berkunci string. Gunakan pemeriksaan bentuk bersama pemeriksaan perjalanan pulang-pergi setiap kali Anda mengubah kasus atau logika konversi; keduanya membuat representasi yang dimaksud menjadi konkret dan mudah ditinjau.
Sumber
- 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.