有趣的问题。问题似乎是 Swift 的可选链接机制,通常能够改变嵌套字典,会从Any 到[String:Any] 进行必要的类型转换。因此,在访问嵌套元素时,只会变得不可读(因为类型转换):
// E.g. Accessing countries.japan.capital
((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"]
…改变嵌套元素甚至不起作用:
// Want to mutate countries.japan.capital.name.
// The typecasts destroy the mutating optional chaining.
((((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] as? [String:Any])?["name"] as? String) = "Edo"
// Error: Cannot assign to immutable expression
可能的解决方案
这个想法是摆脱无类型字典并将其转换为强类型结构,其中每个元素都具有相同的类型。我承认这是一个强硬的解决方案,但最终效果很好。
具有关联值的枚举适用于我们替换无类型字典的自定义类型:
enum KeyValueStore {
case dict([String: KeyValueStore])
case array([KeyValueStore])
case string(String)
// Add more cases for Double, Int, etc.
}
枚举对每个预期的元素类型都有一个案例。这三个案例涵盖了您的示例,但可以轻松扩展以涵盖更多类型。
接下来,我们定义两个下标,一个用于对字典的键控访问(使用字符串),一个用于对数组的索引访问(使用整数)。下标分别检查self 是否为.dict 或.array,如果是,则返回给定键/索引处的值。如果类型不匹配,它们会返回 nil,例如如果您尝试访问具有 .string 值的键。下标也有设置器。这是使链式变异起作用的关键:
extension KeyValueStore {
subscript(_ key: String) -> KeyValueStore? {
// If self is a .dict, return the value at key, otherwise return nil.
get {
switch self {
case .dict(let d):
return d[key]
default:
return nil
}
}
// If self is a .dict, mutate the value at key, otherwise ignore.
set {
switch self {
case .dict(var d):
d[key] = newValue
self = .dict(d)
default:
break
}
}
}
subscript(_ index: Int) -> KeyValueStore? {
// If self is an array, return the element at index, otherwise return nil.
get {
switch self {
case .array(let a):
return a[index]
default:
return nil
}
}
// If self is an array, mutate the element at index, otherwise 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"]
]
]
// Now optional chaining works:
keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("tokyo"))
keyValueStore["countries"]?["japan"]?["capital"]?["name"] = "Edo"
keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("Edo"))
keyValueStore["airports"]?["germany"]?[1] // .some(.string("MUC"))
keyValueStore["airports"]?["germany"]?[1] = "BER"
keyValueStore["airports"]?["germany"]?[1] // .some(.string("BER"))
// Remove value from array by assigning nil. I'm not sure if this makes sense.
keyValueStore["airports"]?["germany"]?[1] = nil
keyValueStore["airports"]?["germany"] // .some(array([.string("FRA"), .string("HAM"), .string("TXL")]))