使用自定义键类型对Swift字典进行编码和解码
如今Codable,几乎每个项目都在使用,该项目于2017年随Swift 4引入。
快进到2021年,Codable仍然可以令我们感到惊讶:
在本文中,让我们看一下有关带有自定义键类型的字典的鲜为人知的行为。
我们将专注于json示例,同样适用于plists。
字典101
Swift字典是键值元素的通用集合,其中Key类型需要符合Hashable(出于性能原因),并且Value类型没有限制。
这是一个具有四个键值对的示例:
SwiftUI技术交流QQ群:518696470
let dictionary: [String: Int] = [
"f": 1,
"i": 2,
"v": 3,
"e": 4
]
当涉及json时,以上相同的字典将具有以下格式:
SwiftUI技术交流QQ群:518696470
{
"f": 1,
"i": 2,
"v": 3,
"e": 4
}
必须在其中引号的位置。
惊喜
假设我们正在构建一个小型应用程序,其中每个模型具有不同语言的不同名称,所有名称都将随意显示给用户。
存储所有名称变体的一种方法是引入一种新Codable Language类型,用作我们模型名称的字典键:
enum Language: String, Codable {
case english
case japanese
case thai
}
然后,我们将以这种方式使用它:
let names: [Language: String] = [
.english: "Victory Monument",
.japanese: "戦勝記念塔",
.thai: "อนุสาวรีย์ชัยสมรภูมิ"
]
一切都很好……直到是时候将其编码为json了:
let encodedDictionary = try JSONEncoder().encode(names)
// ["english", "Victory Monument", "japanese", "戦勝記念塔", "thai", "อนุสาวรีย์ชัยสมรภูมิ"]
那是一个数组,不完全是一个字典。
怎么解码呢?
let jsonData = """
{
"english": "Victory Monument",
"japanese": "戦勝記念塔",
"thai": "อนุสาวรีย์ชัยสมรภูมิ"
}
""".data(using: .utf8)!
let decoded = try JSONDecoder().decode([Language: String].self, from: jsonData)
// typeMismatch(
// Swift.Array<Any>,
// Swift.DecodingError.Context(
// codingPath: [],
// debugDescription: "Expected to decode Array<Any> but found a dictionary instead.",
// underlyingError: nil
// )
// )
解码失败:尽管试图解码成字典([Language: String]),Swift仍希望对数组进行解码。
如果我们更换密钥类型Language有String,那么一切都按预期工作:这是为什么?
经过一番挖掘,事实证明这种行为实际上是预期的:
- 由于Codable类型可以编码为任何内容(包括另一个字典),因此只有在Key类型为String或时,Swift才会将Swift字典编码为json / plist字典Int。
- 所有其他String非Int Key类型或非类型的Swift字典都将被编码为交替键和值的数组。
这既解释了上述解码中的错误,又解释了“意外”编码。
四种解决方案
至此,我们知道,只有字典的键为aString或a时,Swift字典才可将其编码为json字典/从json字典进行解码Int,如何在示例中克服这一问题?让我们看一些解决方案。
经典swift方式
首先是放弃我们模型的json字典表示,并使用预期的交替键和值数组:
// Encoding
let names: [Language: String] = [
.english: "Victory Monument",
.japanese: "戦勝記念塔",
.thai: "อนุสาวรีย์ชัยสมรภูมิ"
]
let encoded = try JSONEncoder().encode(names)
// ["english", "Victory Monument", "japanese", "戦勝記念塔", "thai", "อนุสาวรีย์ชัยสมรภูมิ"]
// Decoding
let jsonData = """
[
"english", "Victory Monument",
"japanese", "戦勝記念塔",
"thai", "อนุสาวรีย์ชัยสมรภูมิ"
]
""".data(using: .utf8)!
let decoded = try JSONDecoder().decode([Language: String].self, from: jsonData)
// [
// .english: "Victory Monument",
// .japanese: "戦勝記念塔",
// .thai: "อนุสาวรีย์ชัยสมรภูมิ"
// ]
当它是同时存储和读取数据的应用程序时,
这种方法非常有用:目前我们并不真正在乎数据的存储方式,Swift会为我们处理编码和解码。
使用整数/字符串
虽然上述解决方案在仅本地使用数据时效果很好,但对于大多数使用而言,json对象可能来自服务器,这意味着我们将收到json字典。
在不更改预期json结构的情况下修补此问题的最简单方法是使用String或Int代替我们的自定义类型:
// Encoding
let names: [String: String] = [
"english": "Victory Monument",
"japanese": "戦勝記念塔",
"thai": "อนุสาวรีย์ชัยสมรภูมิ"
]
let encodedDictionary = try JSONEncoder().encode(names)
// {
// "english": "Victory Monument",
// "japanese": "戦勝記念塔",
// "thai": "อนุสาวรีย์ชัยสมรภูมิ"
// }
// Decoding
let jsonData = """
{
"english": "Victory Monument",
"japanese": "戦勝記念塔",
"thai": "อนุสาวรีย์ชัยสมรภูมิ"
}
""".data(using: .utf8)!
let decoded = try JSONDecoder().decode([String: String].self, from: jsonData)
// [
// "english": "Victory Monument",
// "japanese": "戦勝記念塔",
// "thai": "อนุสาวรีย์ชัยสมรภูมิ"
// ]
但这意味着:
放弃我们明确的声明/期望,也就是Language示例中可能出现的情况的列表
引入了无效/意外密钥(从服务器到服务器)的可能性。
到目前为止,这两种解决方案都不是一般情况下的理想选择,让我们看看下一步我们可以做什么。
自定义编码/解码
没办法解决:
如果我们想从json字典中编码/解码,我们将不得不通过Swift字典使用String或Int作为其Key类型。
话虽如此,我们可以仅将此类密钥类型用于编码/解码,然后使用我们的自定义类型将其存储在Swift中。
让我们围绕我们的字典创建一个新的包装器,这个包装器懒惰地命名为DictionaryWrapper,它将:
String在对它们进行编码之前将其转换为s
解码[String: String]字典然后变成[Language: String]字典
public struct DictionaryWrapper: Codable {
var dictionary: [Language: String]
init(dictionary: [Language: String]) {
self.dictionary = dictionary
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let stringDictionary = try container.decode([String: String].self)
dictionary = [:]
for (stringKey, value) in stringDictionary {
guard let key = Language(rawValue: stringKey) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid key '\(stringKey)'"
)
}
dictionary[key] = value
}
}
public func encode(to encoder: Encoder) throws {
let stringDictionary: [String: String] = Dictionary(
uniqueKeysWithValues: dictionary.map { ($0.rawValue, $1) }
)
var container = encoder.singleValueContainer()
try container.encode(stringDictionary)
}
}
由于这个定义:
- 我们不能再编码/解码无效密钥
- 符合我们最初的期望
现在,我们可以回到原始示例,并看到此新定义将按预期工作:// Encoding let names = DictionaryWrapper( dictionary: [ .english: "Victory Monument", .japanese: "戦勝記念塔", .thai: "อนุสาวรีย์ชัยสมรภูมิ" ] ) let encodedDictionary = try JSONEncoder().encode(names) // { // "english": "Victory Monument", // "japanese": "戦勝記念塔", // "thai": "อนุสาวรีย์ชัยสมรภูมิ" // } // Decoding let jsonData = """ { "english": "Victory Monument", "japanese": "戦勝記念塔", "thai": "อนุสาวรีย์ชัยสมรภูมิ" } """.data(using: .utf8)! let decoded = try JSONDecoder().decode(DictionaryWrapper.self, from: jsonData) // [ // .english: "Victory Monument", // .japanese: "戦勝記念塔", // .thai: "อนุสาวรีย์ชัยสมรภูมิ" // ]
RawRapresentable
最后一个解决方案效果很好,但是对于特定的字典类型却进行了硬编码。将其扩展到更一般情况的一种方法是使用Swift的RawRepresentable协议,该协议表示可以与关联的原始值进行相互转换的类型:
public protocol RawRepresentable {
/// The raw type that can be used to represent all values of the conforming type.
associatedtype RawValue
/// Creates a new instance with the specified raw value.
init?(rawValue: Self.RawValue)
/// The corresponding value of the raw type.
var rawValue: Self.RawValue { get }
}
我们可以DictionaryWrapper在需要字典Key符合的地方定义通用RawRepresentable,并在RawValue字典编码/解码时使用其关联的类型:
public struct DictionaryWrapper<Key: Hashable & RawRepresentable, Value: Codable>: Codable where Key.RawValue: Codable & Hashable {
public var dictionary: [Key: Value]
public init(dictionary: [Key: Value]) {
self.dictionary = dictionary
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawKeyedDictionary = try container.decode([Key.RawValue: Value].self)
dictionary = [:]
for (rawKey, value) in rawKeyedDictionary {
guard let key = Key(rawValue: rawKey) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid key: cannot initialize '\(Key.self)' from invalid '\(Key.RawValue.self)' value '\(rawKey)'")
}
dictionary[key] = value
}
}
public func encode(to encoder: Encoder) throws {
let rawKeyedDictionary = Dictionary(uniqueKeysWithValues: dictionary.map { ($0.rawValue, $1) })
var container = encoder.singleValueContainer()
try container.encode(rawKeyedDictionary)
}
}
由于有了这个新的定义DictionaryWrapper,只要所使用的键的关联RawValue类型为String或,就可以将任何Swift字典编码为json / plist字典Int。
更优解决方案 a property wrapper!
我们的字典很可能将成为需要编码/解码的模型的一部分。除了添加DictionaryWrapper作为每个属性类型定义的一部分之外,我们还可以DictionaryWrapper通过以下方式更新定义:
- 将@propertyWrapper属性添加到我们的DictionaryWrapper声明中
-
将内部dictionary属性名称替换为wrappedValue
这就是制作DictionaryWrapper属性包装器所需的全部内容:@propertyWrapper public struct DictionaryWrapper<Key: Hashable & RawRepresentable, Value: Codable>: Codable where Key.RawValue: Codable & Hashable { public var wrappedValue: [Key: Value] public init() { wrappedValue = [:] } public init(wrappedValue: [Key: Value]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawKeyedDictionary = try container.decode([Key.RawValue: Value].self) wrappedValue = [:] for (rawKey, value) in rawKeyedDictionary { guard let key = Key(rawValue: rawKey) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Invalid key: cannot initialize '\(Key.self)' from invalid '\(Key.RawValue.self)' value '\(rawKey)'") } wrappedValue[key] = value } } public func encode(to encoder: Encoder) throws { let rawKeyedDictionary = Dictionary(uniqueKeysWithValues: wrappedValue.map { ($0.rawValue, $1) }) var container = encoder.singleValueContainer() try container.encode(rawKeyedDictionary) } }
进行此更改后,我们可以继续使用此新属性包装器声明任何模型:
struct FSModel: Codable {
@DictionaryWrapper var names: [Language: String]
...
}
…然后直接访问字典,而不必进行详尽的操作DictionaryWrapper:
let jsonData = """
{
"names": {
"english": "Victory Monument",
"japanese": "戦勝記念塔",
"thai": "อนุสาวรีย์ชัยสมรภูมิ"
},
...
}
""".data(using: .utf8)!
let model = try JSONDecoder().decode(FSModel.self, from: jsonData)
model.names // [Language: String]
// [
// .english: "Victory Monument",
// .japanese: "戦勝記念塔",
// .thai: "อนุสาวรีย์ชัยสมรภูมิ"
// ]
结论
Codable Swift的引入是一件幸运的事:尽管在这里和那里都有一些缺点,但它也足够灵活,我们可以扩展它并使其满足我们的需求。
原文链接
加入我们一起学习SwiftUI
QQ:3365059189
SwiftUI技术交流QQ群:518696470
教程网站:www.openswiftui.com