从字典中删除嵌套键

scr*_*rrr 8 collections dictionary nsdictionary ios swift

假设我有一个相当复杂的字典,就像这个:

let dict: [String: Any] = [
    "countries": [
        "japan": [
            "capital": [
                "name": "tokyo",
                "lat": "35.6895",
                "lon": "139.6917"
            ],
            "language": "japanese"
        ]
    ],
    "airports": [
        "germany": ["FRA", "MUC", "HAM", "TXL"]
    ]
]
Run Code Online (Sandbox Code Playgroud)

我可以使用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
}
Run Code Online (Sandbox Code Playgroud)

当然必须有一个更优雅的方式?

理想情况下,我会编写一个测试助手,它接受一个KVC字符串并具有如下签名:

func dictWithoutKeyPath(_ path: String) -> [String: Any] 
Run Code Online (Sandbox Code Playgroud)

"lat"我打电话的情况下 dictWithoutKeyPath("countries.japan.capital.lat").

小智 6

使用下标时,如果下标是get/set且变量是可变的,则整个表达式是可变的.但是,由于类型转换,表达"失去"可变性.(它不再是l值).

解决此问题的最短方法是创建一个get/set下标并为您进行转换.

extension Dictionary {
    subscript(jsonDict key: Key) -> [String:Any]? {
        get {
            return self[key] as? [String:Any]
        }
        set {
            self[key] = newValue as? Value
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以编写以下内容:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?[jsonDict: "capital"]?["name"] = "berlin"
Run Code Online (Sandbox Code Playgroud)

我们非常喜欢这个问题,所以我们决定制作一个关于它的(公共)Swift Talk剧集:改变无类字典


dfr*_*fri 2

您可以构造递归方法(读/写),通过重复尝试将(子)字典值转换为字典本身来访问给定的关键路径[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 }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

设置示例

// 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"]
    ]
]
Run Code Online (Sandbox Code Playgroud)

用法示例:

// 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"]
        ]
    ] */
Run Code Online (Sandbox Code Playgroud)

请注意,如果为分配(使用 setter)提供的键路径不存在,则这不会导致等效嵌套字典的构造,而只是导致字典不发生变化。