我们说我有一个相当复杂的字典,就像这个:
let dict: [String: Any] = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
我可以使用if let ..
块访问所有字段,可选择在阅读时投射到我可以使用的内容。
但是,我目前正在编写单元测试,我需要以多种方式选择性地中断字典。
但我不知道如何优雅地从字典中删除密钥。
例如,我想在一次测试中移除密钥"japan"
,在下一个"lat"
中应该为零。
这是我目前删除"lat"
的实现:
if var countries = dict["countries"] as? [String: Any],
var japan = countries["japan"] as? [String: Any],
var capital = japan["capital"] as? [String: Any]
{
capital.removeValue(forKey: "lat")
japan["capital"] = capital
countries["japan"] = japan
dictWithoutLat["countries"] = countries
}
当然必须有更优雅的方式吗?
理想情况下,我会编写一个测试帮助程序,它接受一个KVC字符串并具有如下签名:
func dictWithoutKeyPath(_ path: String) -> [String: Any]
在"lat"
案例中,我将其与dictWithoutKeyPath("countries.japan.capital.lat")
一起使用。
答案 0 :(得分:6)
使用下标时,如果下标是get / set且变量是可变的,则整个表达式是可变的。但是,由于类型转换,表达式"失败"可变性。 (它不再是l-value了。
解决此问题的最简单方法是创建一个获取/设置的下标并为您进行转换。
extension Dictionary {
subscript(jsonDict key: Key) -> [String:Any]? {
get {
return self[key] as? [String:Any]
}
set {
self[key] = newValue as? Value
}
}
}
现在您可以写下以下内容:
dict[jsonDict: "countries"]?[jsonDict: "japan"]?[jsonDict: "capital"]?["name"] = "berlin"
我们非常喜欢这个问题,所以我们决定制作关于它的(公开)Swift Talk剧集:mutating untyped dictionaries
答案 1 :(得分:1)
您可以通过反复尝试将(子)字典值转换为[Key: Any]
字典本身来构建访问您给定密钥路径的递归方法(读/写)。此外,允许通过新的subscript
公开访问这些方法。
请注意,您可能必须明确导入Foundation
才能访问components(separatedBy:)
(桥接)的String
方法。
extension Dictionary {
subscript(keyPath keyPath: String) -> Any? {
get {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath)
else { return nil }
return getValue(forKeyPath: keyPath)
}
set {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath),
let newValue = newValue else { return }
self.setValue(newValue, forKeyPath: keyPath)
}
}
static private func keyPathKeys(forKeyPath: String) -> [Key]? {
let keys = forKeyPath.components(separatedBy: ".")
.reversed().flatMap({ $0 as? Key })
return keys.isEmpty ? nil : keys
}
// recursively (attempt to) access queried subdictionaries
// (keyPath will never be empty here; the explicit unwrapping is safe)
private func getValue(forKeyPath keyPath: [Key]) -> Any? {
guard let value = self[keyPath.last!] else { return nil }
return keyPath.count == 1 ? value : (value as? [Key: Any])
.flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) }
}
// recursively (attempt to) access the queried subdictionaries to
// finally replace the "inner value", given that the key path is valid
private mutating func setValue(_ value: Any, forKeyPath keyPath: [Key]) {
guard self[keyPath.last!] != nil else { return }
if keyPath.count == 1 {
(value as? Value).map { self[keyPath.last!] = $0 }
}
else if var subDict = self[keyPath.last!] as? [Key: Value] {
subDict.setValue(value, forKeyPath: Array(keyPath.dropLast()))
(subDict as? Value).map { self[keyPath.last!] = $0 }
}
}
}
示例设置
// your example dictionary
var dict: [String: Any] = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
使用示例:
// read value for a given key path
let isNil: Any = "nil"
print(dict[keyPath: "countries.japan.capital.name"] ?? isNil) // tokyo
print(dict[keyPath: "airports"] ?? isNil) // ["germany": ["FRA", "MUC", "HAM", "TXL"]]
print(dict[keyPath: "this.is.not.a.valid.key.path"] ?? isNil) // nil
// write value for a given key path
dict[keyPath: "countries.japan.language"] = "nihongo"
print(dict[keyPath: "countries.japan.language"] ?? isNil) // nihongo
dict[keyPath: "airports.germany"] =
(dict[keyPath: "airports.germany"] as? [Any] ?? []) + ["FOO"]
dict[keyPath: "this.is.not.a.valid.key.path"] = "notAdded"
print(dict)
/* [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lon": "139.6917",
"lat": "35.6895"
],
"language": "nihongo"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL", "FOO"]
]
] */
请注意,如果赋值(使用setter)不存在提供的键路径,则不会导致构造等效的嵌套字典,只会导致字典没有变异。
答案 2 :(得分:1)
有趣的问题。问题似乎是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")]))
答案 3 :(得分:1)
我想跟进my previous answer另一个解决方案。这个扩展了Swift的Dictionary
类型,带有一个带有关键路径的新下标。
我首先介绍一个名为KeyPath
的新类型来表示关键路径。它并不是绝对必要的,但它使得使用关键路径变得更加容易,因为它允许我们将关键路径拆分为其组件的逻辑。
import Foundation
/// Represents a key path.
/// Can be initialized with a string of the form "this.is.a.keypath"
///
/// We can't use Swift's #keyPath syntax because it checks at compilet time
/// if the key path exists.
struct KeyPath {
var elements: [String]
var isEmpty: Bool { return elements.isEmpty }
var count: Int { return elements.count }
var path: String {
return elements.joined(separator: ".")
}
func headAndTail() -> (String, KeyPath)? {
guard !isEmpty else { return nil }
var tail = elements
let head = tail.removeFirst()
return (head, KeyPath(elements: tail))
}
}
extension KeyPath {
init(_ string: String) {
elements = string.components(separatedBy: ".")
}
}
extension KeyPath: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(value)
}
init(unicodeScalarLiteral value: String) {
self.init(value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
接下来,我创建一个名为StringProtocol
的虚拟协议,我们稍后需要约束我们的Dictionary
扩展名。 Swift 3.0尚不支持将泛型参数约束为具体类型(例如extension Dictionary where Key == String
)的泛型类型的扩展。计划对Swift 4.0提供支持,但在此之前,我们需要这个小解决方法:
// We need this because Swift 3.0 doesn't support extension Dictionary where Key == String
protocol StringProtocol {
init(string s: String)
}
extension String: StringProtocol {
init(string s: String) {
self = s
}
}
现在我们可以编写新的下标。 getter和setter的实现相当长,但它们应该很简单:我们从头到尾遍历键路径,然后在该位置获取/设置值:
// We want extension Dictionary where Key == String, but that's not supported yet,
// so work around it with Key: StringProtocol.
extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
get {
guard let (head, remainingKeyPath) = keyPath.headAndTail() else {
return nil
}
let key = Key(string: head)
let value = self[key]
switch remainingKeyPath.isEmpty {
case true:
// Reached the end of the key path
return value
case false:
// Key path has a tail we need to traverse
switch value {
case let nestedDict as [Key: Any]:
// Next nest level is a dictionary
return nestedDict[keyPath: remainingKeyPath]
default:
// Next nest level isn't a dictionary: invalid key path, abort
return nil
}
}
}
set {
guard let (head, remainingKeyPath) = keyPath.headAndTail() else {
return
}
let key = Key(string: head)
// Assign new value if we reached the end of the key path
guard !remainingKeyPath.isEmpty else {
self[key] = newValue as? Value
return
}
let value = self[key]
switch value {
case var nestedDict as [Key: Any]:
// Key path has a tail we need to traverse
nestedDict[keyPath: remainingKeyPath] = newValue
self[key] = nestedDict as? Value
default:
// Invalid keyPath
return
}
}
}
}
这就是它的用法:
var dict: [String: Any] = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
dict[keyPath: "countries.japan"] // ["language": "japanese", "capital": ["lat": "35.6895", "name": "tokyo", "lon": "139.6917"]]
dict[keyPath: "countries.someothercountry"] // nil
dict[keyPath: "countries.japan.capital"] // ["lat": "35.6895", "name": "tokyo", "lon": "139.6917"]
dict[keyPath: "countries.japan.capital.name"] // "tokyo"
dict[keyPath: "countries.japan.capital.name"] = "Edo"
dict[keyPath: "countries.japan.capital.name"] // "Edo"
dict[keyPath: "countries.japan.capital"] // ["lat": "35.6895", "name": "Edo", "lon": "139.6917"]
我真的很喜欢这个解决方案。这是相当多的代码,但你只需要编写一次,我认为它看起来非常好用。
答案 4 :(得分:0)
将你的字典传递给这个函数,它会返回一个扁平的字典,没有任何嵌套的字典。
// SWIFT 3.0
$lookup