【问题标题】:Remove nested key from dictionary从字典中删除嵌套键
【发布时间】:2017-03-08 19:04:39
【问题描述】:

假设我有一本相当复杂的字典,比如这个:

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" 应该是 nil。

这是我当前删除 "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") 调用它。

【问题讨论】:

  • 你的字典每次都是一样的,我会为你提供一个解决方案。
  • dfri:对不起,不是真的。我忙于其他事情,但自发地,您的答案不起作用(在操场上)。我想语法不是很正确,不应该是“扩展字典,其中键:ExpressibleByStringLiteral,值:任何”?无论如何,我希望我明天可以花一些时间。

标签: ios swift dictionary collections nsdictionary


【解决方案1】:

当使用下标时,如果下标是 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

【讨论】:

    【解决方案2】:

    我想跟进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 的实现相当长,但它们应该很简单:我们从头到尾遍历 key 路径,然后在该位置获取/设置值:

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

    我真的很喜欢这个解决方案。代码不少,但你只需要写一次,我觉得用起来很好看。

    【讨论】:

    • 你不会说使用可变参数参数比包含句点作为分隔符的单个字符串更“快捷”吗?它可以防止一些打字错误,并允许您的字典键包含句点。
    • 我不确定。恐怕路径的分层性质会在逗号分隔的列表中丢失。但它肯定是另一种选择。
    【解决方案3】:

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

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

    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)提供的键路径不存在,这将不会导致构建等效的嵌套字典,而只会导致字典没有突变。

    【讨论】:

      【解决方案4】:

      有趣的问题。问题似乎是 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")]))
      

      【讨论】:

        【解决方案5】:

        将你的字典传递给这个函数,它会返回一个平面字典,没有任何嵌套的字典。

        //SWIFT 3.0

        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
            }
        

        【讨论】:

          猜你喜欢
          • 2019-11-18
          • 2014-01-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-09-17
          • 2018-04-14
          • 1970-01-01
          相关资源
          最近更新 更多