问题描述
假设我有一个相当复杂的字典,像这样:
let dict:[String:Any]
countries:[
japan:[
capital:[
name:tokyo,
lat:35.6895 ,
lon:139.6917
],
language:japanese
]
],
b $ bgermany:[FRA,MUC,HAM,TXL]
]
]
我可以访问的所有字段,如果let ..
阻止可选择地转换为我可以使用的东西,当阅读时。
但是,我目前正在编写单元测试,我需要以多种方式选择性地打破字典。
但我不知道如何从字典中优雅地删除键。
在一个测试中删除键japan
,在下一个lat
中应该为nil。
这是我当前的实现,用于删除lat
:
如果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]
在 c> case我用
dictWithoutKeyPath(countries.japan.capital.lat)
。
解决方案
您可以构造一个递归方法,通过重复尝试将(子)字典值转换为
[Key:Any]
例如
/ * general路径扩展* /
扩展字典{
func newDictByRemovingValue(forKeyPath keyPath:[Key])
- > [Key:Any] {
return removeValue(inDict:self,
forKeyPath:Array(keyPath.reversed()))
}
//递归to)访问查询的子字典
fileprivate func removeValue(inDict dict:[Key:Any],
forKeyPath keyPath:[Key]) - > [Key:Any] {
guard let key = keyPath.last else {return dict}
var dict = dict
如果keyPath.count> 1,let subDict = dict [key] as? [Key:Any] {
dict [key] = removeValue(inDict:subDict,
forKeyPath:Array(keyPath.dropLast()))
return dict
}
dict.removeValue(forKey:key)
return dict
}
}
示例用法(应用于您的问题中发布的
dict
):
let newDictA = dict
.newDictByRemovingValue(forKeyPath:[countries,japan])
print(newDictA)
/ * [
airports:[
germany:[FRA,MUC,HAM,TXL]
],
countries:[:] $ b $同样的方法自然可以应用于其他 Key
类型,例如 Int
键: ... .newDictByRemovingValue(forKeyPath: 1,3])
ExpressibleByStringLiteral
(如在您的具体示例中: String
keys),您可以实现另一个助手(利用递归 Dictionary
方法)允许您在countries.japan.capital.lat
表单中指定密钥路径:
/ *字符串文字特定的键路径扩展名* /
扩展名字典其中键:ExpressibleByStringLiteral {
func newDictByRemovingValue(forKeyPath keyPath:String)
- > [Key:Any]? {
let keyPathArr = keyPath.components(separatedBy:。)
.reversed()。flatMap {$ 0 as? Key}
guard!keyPathArr.isEmpty else {return nil}
return removeValue(inDict:self,forKeyPath:keyPathArr)
}
}
使用示例:
如果let newDictB = dict
.newDictByRemovingValue(forKeyPath:countries.japan.capital.lat){
print(newDictB)
}
/ * [
countries [
japan:[
capital:[
name:tokyo,
lon:139.6917
],
language:japanese
]
],
airports:[
germany:[FRA,MUC,HAM TXL]
]
] * /
Let's say I have a rather complex dictionary, like this one:
let dict: [String: Any] = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
I can access all fields with if let ..
blocks optionally casting to something that I can work with, when reading.
However, I am currently writing unit tests where I need to selectively break dictionaries in multiple ways.
But I don't know how to elegantly remove keys from the dictionary.
For example I want to remove the key "japan"
in one test, in the next "lat"
should be nil.
Here's my current implementation for removing "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
}
Surely there must be a more elegant way?
Ideally I'd write a test helper that takes a KVC string and has a signature like:
func dictWithoutKeyPath(_ path: String) -> [String: Any]
In the "lat"
case I'd call it with dictWithoutKeyPath("countries.japan.capital.lat")
.
解决方案 You could construct a recursive method which visits your given key path by repeatedly attempting conversions of (sub-)dictionary values to [Key: Any]
dictionaries themselves.
E.g.
/* general "key path" extension */
extension Dictionary {
func newDictByRemovingValue(forKeyPath keyPath: [Key])
-> [Key: Any] {
return removeValue(inDict: self,
forKeyPath: Array(keyPath.reversed()))
}
// recursively (attempt to) access queried subdictionaries
fileprivate func removeValue(inDict dict: [Key: Any],
forKeyPath keyPath: [Key]) -> [Key: Any] {
guard let key = keyPath.last else { return dict }
var dict = dict
if keyPath.count > 1, let subDict = dict[key] as? [Key: Any] {
dict[key] = removeValue(inDict: subDict,
forKeyPath: Array(keyPath.dropLast()))
return dict
}
dict.removeValue(forKey: key)
return dict
}
}
Example usage (applied to the dict
posted in your question):
let newDictA = dict
.newDictByRemovingValue(forKeyPath: ["countries", "japan"])
print(newDictA)
/* [
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
],
"countries": [:]
] */
The same method could naturally be applied in nested dictionaries with other Key
types, e.g. for Int
keys:
... .newDictByRemovingValue(forKeyPath: [1, 3])
For keys that conforms to ExpressibleByStringLiteral
(as in your specific example: String
keys), you could implement another helper (making use of the recursive Dictionary
method above) to allow you to specify you key path in the "countries.japan.capital.lat"
form:
/* String literal specific "key path" extension */
extension Dictionary where Key: ExpressibleByStringLiteral {
func newDictByRemovingValue(forKeyPath keyPath: String)
-> [Key: Any]? {
let keyPathArr = keyPath.components(separatedBy: ".")
.reversed().flatMap { $0 as? Key }
guard !keyPathArr.isEmpty else { return nil }
return removeValue(inDict: self, forKeyPath: keyPathArr)
}
}
Example usage:
if let newDictB = dict
.newDictByRemovingValue(forKeyPath: "countries.japan.capital.lat") {
print(newDictB)
}
/* [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
] */
这篇关于从字典中删除嵌套键的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!