有趣的问题。问题似乎在于Swift的可选链机制通常可以改变嵌套字典,但会因必要的类型转换从到[String:Any]而出错。因此,尽管访问嵌套元素变得不可读(因为类型转换),但仍然可能发生错误。
// E.g. Accessing countries.japan.capital
((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"]
变异嵌套元素甚至不起作用:
…
((((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] as? [String:Any])?["name"] as? String) = "Edo"
可能的解决方案
思路是摆脱无类型字典,并将其转换为强类型结构,其中每个元素具有相同的类型。我承认这是一种笨重的解决方案,但最终效果很好。
带有关联值的枚举对于替换无类型字典的自定义类型非常有效:
enum KeyValueStore {
case dict([String: KeyValueStore])
case array([KeyValueStore])
case string(String)
}
枚举类型有每个预期元素类型的一个实例。这三种情况涵盖了您的示例,但很容易扩展以涵盖更多类型。
接下来,我们定义两个下标,一个用于按键访问字典(使用字符串),另一个用于按索引访问数组(使用整数)。下标会检查
self
是否为
.dict
或
.array
,如果是,则返回给定键/索引处的值。如果类型不匹配,例如尝试访问
.string
值的键,则它们将返回
nil
。下标还具有设置器。这对于使链接的突变起作用至关重要。
extension KeyValueStore {
subscript(_ key: String) -> KeyValueStore? {
get {
switch self {
case .dict(let d):
return d[key]
default:
return nil
}
}
set {
switch self {
case .dict(var d):
d[key] = newValue
self = .dict(d)
default:
break
}
}
}
subscript(_ index: Int) -> KeyValueStore? {
get {
switch self {
case .array(let a):
return a[index]
default:
return nil
}
}
set {
switch self {
case .array(var a):
if let v = newValue {
a[index] = v
} else {
a.remove(at: index)
}
self = .array(a)
default:
break
}
}
}
}
最后,我们添加了一些方便的初始化器,用于使用字典、数组或字符串字面量初始化我们的类型。这些并非必需,但使得使用该类型更加容易:
extension KeyValueStore: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (String, KeyValueStore)...) {
var dict: [String: KeyValueStore] = [:]
for (key, value) in elements {
dict[key] = value
}
self = .dict(dict)
}
}
extension KeyValueStore: ExpressibleByArrayLiteral {
init(arrayLiteral elements: KeyValueStore...) {
self = .array(elements)
}
}
extension KeyValueStore: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self = .string(value)
}
init(extendedGraphemeClusterLiteral value: String) {
self = .string(value)
}
init(unicodeScalarLiteral value: String) {
self = .string(value)
}
}
这里是例子:
var keyValueStore: KeyValueStore = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
keyValueStore["countries"]?["japan"]?["capital"]?["name"]
keyValueStore["countries"]?["japan"]?["capital"]?["name"] = "Edo"
keyValueStore["countries"]?["japan"]?["capital"]?["name"]
keyValueStore["airports"]?["germany"]?[1]
keyValueStore["airports"]?["germany"]?[1] = "BER"
keyValueStore["airports"]?["germany"]?[1]
keyValueStore["airports"]?["germany"]?[1] = nil
keyValueStore["airports"]?["germany"]