关于ios:从字典中删除嵌套密钥

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"]
    ]
]

我可以使用if let ..块访问所有字段,也可以选择在阅读时转换到我可以使用的对象。

但是,我目前正在编写单元测试,在这里我需要有选择地以多种方式破坏字典。

但我不知道如何优雅地从字典中删除键。

例如,我想在一个测试中删除键"japan",在下一个测试中,"lat"应该为零。

下面是我当前删除"lat"的实现:

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]

"lat"的情况下,我会用dictWithoutKeyPath("countries.japan.capital.lat")来称呼它。


使用下标时,如果下标是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的Dictionary类型,带有一个新的下标,它采用了一个键路径。

我首先介绍了一个名为KeyPath的新类型来表示密钥路径。这并不是严格必要的,但它使处理密钥路径变得容易得多,因为它让我们将密钥路径拆分为其组件的逻辑包装起来。

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)
    }
}

接下来,我创建一个名为StringProtocol的虚拟协议,稍后我们需要约束Dictionary扩展。Swift 3.0还不支持将泛型参数约束到具体类型(如extension Dictionary where Key == String)的泛型类型的扩展。对这一点的支持计划在Swift4.0上进行,但在此之前,我们需要这个小小的解决方案:

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的可选链接机制(通常能够改变嵌套字典)会从Any[String:Any]跳过必要的类型转换。因此,当访问嵌套元素时,只能无法读取(因为类型转换):

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.
}

对于每个预期的元素类型,枚举都有一个事例。这三个案例涵盖了您的示例,但是可以很容易地扩展到更多类型。

接下来,我们定义两个下标,一个用于键控访问字典(使用字符串),另一个用于索引访问数组(使用整数)。下标检查self是否分别是.dict.array,如果是,则返回给定键/索引的值。如果类型不匹配,则返回nil,例如,如果您试图访问.string值的密钥。下标也有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
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")]))


您可以构造递归方法(读/写),通过反复尝试将(子)字典值转换为[Key: Any]字典本身,来访问给定的键路径。此外,允许公众通过新的subscript访问这些方法。

注意,您可能需要显式导入Foundation才能访问Stringcomponents(separatedBy:)方法(桥接)。

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
    }