Remove nested key from dictionary
比如说,我有一本相当复杂的字典,比如这本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | let dict: [String: Any] = [ "countries": [ "japan": [ "capital": [ "name":"tokyo", "lat":"35.6895", "lon":"139.6917" ], "language":"japanese" ] ], "airports": [ "germany": ["FRA","MUC","HAM","TXL"] ] ] |
我可以使用
但是,我目前正在编写单元测试,在这里我需要有选择地以多种方式破坏字典。
但我不知道如何优雅地从字典中删除键。
例如,我想在一个测试中删除键
下面是我当前删除
1 2 3 4 5 6 7 8 9 | 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字符串并具有如下签名:
1 | func dictWithoutKeyPath(_ path: String) -> [String: Any] |
在
使用下标时,如果下标是get/set且变量是可变的,则整个表达式是可变的。但是,由于类型转换,表达式"丢失"了可变性。(不再是L值了)。
解决这个问题的最短方法是创建一个下标,它是get/set,并为您进行转换。
1 2 3 4 5 6 7 8 9 10 | extension Dictionary { subscript(jsonDict key: Key) -> [String:Any]? { get { return self[key] as? [String:Any] } set { self[key] = newValue as? Value } } } |
号
现在您可以编写以下内容:
1 | dict[jsonDict:"countries"]?[jsonDict:"japan"]?[jsonDict:"capital"]?["name"] ="berlin" |
号
我们非常喜欢这个问题,所以我们决定做一个(公开的)关于它的快速谈话集:改变非类型化词典
我想用另一个解决方案来跟进我以前的回答。这一个扩展了Swift的
我首先介绍了一个名为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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) } } |
接下来,我创建一个名为
1 2 3 4 5 6 7 8 9 10 | // 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的实现相当长,但它们应该很简单:我们从头到尾遍历键路径,然后在该位置获取/设置值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // 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 } } } } |
号
这就是它在使用中的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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"] |
号
我真的很喜欢这个解决方案。这是相当多的代码,但你只需要写一次,我认为它在使用中看起来非常好。
有趣的问题。问题似乎在于,Swift的可选链接机制(通常能够改变嵌套字典)会从
1 2 | // E.g. Accessing countries.japan.capital ((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] |
。
…改变嵌套元素甚至不起作用:
1 2 3 4 | // 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 |
可能的解决方案
其思想是去掉非类型化字典,并将其转换为强类型结构,其中每个元素具有相同的类型。我承认这是一个很难解决的问题,但最终效果相当好。
具有关联值的枚举对于替换非类型化字典的自定义类型来说很好:
1 2 3 4 5 6 | enum KeyValueStore { case dict([String: KeyValueStore]) case array([KeyValueStore]) case string(String) // Add more cases for Double, Int, etc. } |
。
对于每个预期的元素类型,枚举都有一个事例。这三个案例涵盖了您的示例,但是可以很容易地扩展到更多类型。
接下来,我们定义两个下标,一个用于键控访问字典(使用字符串),另一个用于索引访问数组(使用整数)。下标检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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 } } } } |
。
最后,我们添加了一些方便的初始值设定项,用于使用字典、数组或字符串文本初始化类型。这些不是严格必要的,但使处理类型更容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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) } } |
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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")])) |
。
您可以构造递归方法(读/写),通过反复尝试将(子)字典值转换为
注意,您可能需要显式导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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 } } } } |
示例设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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"] ] ] |
号
示例用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // 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),这将不会导致等效嵌套字典的构造,而只会导致字典没有任何变化。
将字典传递给这个函数,它将返回一个平面字典,不包含任何嵌套的dict。
//斯威夫特3.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | func concatDict( dict: [String: Any])-> [String: Any]{ var dict = dict for (parentKey, parentValue) in dict{ if let insideDict = parentValue as? [String: Any]{ let keys = insideDict.keys.map{ return parentKey + $0 } for (key, value) in zip(keys, insideDict.values) { dict[key] = value } dict[parentKey] = nil dict = concatDict(dict: dict) } } return dict } |