【问题标题】:How to access deeply nested dictionaries in Swift如何在 Swift 中访问深度嵌套的字典
【发布时间】:2014-10-17 23:41:09
【问题描述】:

我的应用中有一个非常复杂的数据结构,我需要对其进行操作。我试图记录玩家在他们的花园里有多少种虫子。虫有十种,每种有十种花纹,每种花纹有十种颜色。所以可能有 1000 个独特的错误,我想跟踪玩家有多少这些类型。嵌套字典如下所示:

var colorsDict: [String : Int]
var patternsDict: [String : Any] // [String : colorsDict]
var bugsDict: [String : Any] // [String : patternsDict]

我没有收到此语法的任何错误或投诉。

当我想增加玩家的错误集合时,这样做:

bugs["ladybug"]["spotted"]["red"]++

我收到此错误:String is not convertible to 'DictionaryIndex',错误的胡萝卜位于第一个字符串下方。

另一个类似的帖子建议使用“as Any?”在代码中,但是该帖子的 OP 只有一个深度的字典,所以可以很容易地做到这一点: dict["string"] as Any? ...

我不确定如何使用多级字典来做到这一点。任何帮助将不胜感激。

【问题讨论】:

    标签: dictionary swift nested


    【解决方案1】:

    使用字典时,您必须记住字典中可能不存在键。出于这个原因,字典总是返回可选项。因此,每次您按键访问字典时,您必须在每个级别展开如下:

    bugsDict["ladybug"]!["spotted"]!["red"]!++
    

    我假设您知道可选选项,但为了清楚起见,如果您 100% 确定该键存在于字典中,请使用感叹号,否则最好使用问号:

    bugsDict["ladybug"]?["spotted"]?["red"]?++
    

    附录:这是我在操场上测试的代码:

    var colorsDict = [String : Int]()
    var patternsDict =  [String : [String : Int]] ()
    var bugsDict = [String : [String : [String : Int]]] ()
    
    colorsDict["red"] = 1
    patternsDict["spotted"] = colorsDict
    bugsDict["ladybug"] = patternsDict
    
    
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 1
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 2
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 3
    bugsDict["ladybug"]!["spotted"]!["red"]! // Prints 4
    

    【讨论】:

    • 这是个好主意,我已经添加了!在每个部分之后,但这并不能解决我的问题。我仍然收到帖子中提到的错误:String is not convertible to DictionaryIndex
    • 在我的回答中查看更新,看看是否有帮助。从未见过你提到的错误,使用任何一个!和?
    • 谢谢安东尼奥!今晚我会深入研究这个问题,并在我的代码中解决问题后接受你的回答。如果由于某种原因我仍然无法获得它,我也会发布...
    • bugsDict["ladybug"]?["spotted"]?["red"]?++ if let spottedDict = bugsDict["ladybug"]?["spotted"], let red = spottedDict?["red"] {} 跨度>
    • @ViruMax Any 没有算术运算符,我认为您必须使表达式更明确,例如bugsDict["ladybug"]!["spotted"]!["red"] = (bugsDict["ladybug"]!["spotted"]!["red"] as! Int) + 1
    【解决方案2】:

    另一种选择:您可以尝试拨打dict.value( forKeyPath: "ladybug.spotted.red" )!


    所以我只是用 Swift 5 尝试了这个:

    import Foundation
    
    var d = [ "ladybug" : [ "spotted" : [ "red" : 123 ] ] ] as [String:Any]
    
    (d as NSDictionary).value(forKeyPath: "ladybug.spotted.red")
    

    它有效,但这可能是最好的方法:

    d["ladybug"]?["spotted"]?["red"]
    

    【讨论】:

    • 应该是(dict as NSDictionary).valueForKeyPath("ladybug.spotted.red")!
    • NSObject 实现了 valueForKeyPath,所以如果有必要,你可以根据什么 dict 转换为 NSObject。 (NSDictionary 实现 objectForKey)
    • dict.valueForKeyPath()valueForKey() 在 Swift 3 中可用?我猜苹果已经删除了它们。
    • 我刚刚在一个全新的项目中使用了它们。语法为dic.value(forKeyPath: "path")
    • 现在取出来了吗?我的编译器说不可用
    【解决方案3】:

    我遇到了同样的问题,我想将 boolValue 嵌套在字典中。

    {
      "Level1": {
        "leve2": {
          "code": 0,
          "boolValue": 1
        }
      }
    }
    

    我尝试了很多解决方案,但这些都对我不起作用,因为我缺少类型转换。所以我使用以下代码从 json 中获取 boolValue,其中 json 是 [String:Any] 类型的嵌套字典。

    let boolValue = ((json["level1"]
        as? [String: Any])?["level2"]
        as? [String: Any])?["boolValue"] as? Bool
    

    【讨论】:

    • 这有点可悲 ;(
    【解决方案4】:

    我的主要用例是读取深度字典中的临时值。在我的 Swift 3.1 项目中,给出的答案都没有对我有用,所以我去寻找并找到了 Ole Begemann 的 Swift 字典的出色扩展,并附有 detailed explanation 说明它的工作原理。

    我用我为使用它而制作的 Swift 文件创建了一个Github gist,欢迎提供反馈。

    要使用它,您可以将 Keypath.swift 添加到您的项目中,然后您可以在任何 [String:Any] 字典上简单地使用 keyPath 下标语法,如下所示。

    考虑到你有一个像这样的 JSON 对象:

    {
        "name":"John",
        "age":30,
        "cars": {
            "car1":"Ford",
            "car2":"BMW",
            "car3":"Fiat"
        }
    }
    

    存储在字典var dict:[String:Any] 中。您可以使用以下语法来获取对象的不同深度。

    if let name = data[keyPath:"name"] as? String{
        // name has "John"
    }
    if let age = data[keyPath:"age"] as? Int{
        // age has 30
    }
    if let car1 = data[keyPath:"cars.car1"] as? String{
        // car1 has "Ford"
    }
    

    请注意,该扩展也支持写入嵌套字典,但我还没有使用过。

    我还没有找到一种方法来使用它访问字典对象中的数组,但这是一个开始!我正在寻找 Swift 的 JSON Pointer 实现,但还没有找到。

    【讨论】:

    • 尚未在 Swift 4 上尝试过此操作。如果您对此有所了解,请发表另一条评论或答案,谢谢!
    【解决方案5】:

    如果它只是关于检索(而不是操作),那么这里是 Swift 3 的字典扩展(准备粘贴到 Xcode 游乐场的代码):

    //extension
    extension Dictionary where Key: Hashable, Value: Any {
        func getValue(forKeyPath components : Array<Any>) -> Any? {
            var comps = components;
            let key = comps.remove(at: 0)
            if let k = key as? Key {
                if(comps.count == 0) {
                    return self[k]
                }
                if let v = self[k] as? Dictionary<AnyHashable,Any> {
                    return v.getValue(forKeyPath : comps)
                }
            }
            return nil
        }
    }
    
    //read json
    let json = "{\"a\":{\"b\":\"bla\"},\"val\":10}" //
    if let parsed = try JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<AnyHashable,Any>
    {
        parsed.getValue(forKeyPath: ["a","b"]) //-> "bla"
        parsed.getValue(forKeyPath: ["val"]) //-> 10
    }
    
    //dictionary with different key types
    let test : Dictionary<AnyHashable,Any> = ["a" : ["b" : ["c" : "bla"]], 0 : [ 1 : [ 2 : "bla"]], "four" : [ 5 : "bla"]]
    test.getValue(forKeyPath: ["a","b","c"]) //-> "bla"
    test.getValue(forKeyPath: ["a","b"]) //-> ["c": "bla"]
    test.getValue(forKeyPath: [0,1,2]) //-> "bla"
    test.getValue(forKeyPath: ["four",5]) //-> "bla"
    test.getValue(forKeyPath: ["a","b","d"]) //-> nil
    
    //dictionary with strings as keys
    let test2 = ["one" : [ "two" : "three"]]
    test2.getValue(forKeyPath: ["one","two"]) //-> "three"
    

    【讨论】:

    • 我敢打赌你可以把它变成一个可变参数函数,那么你就不需要数组语法了......
    • 如何在目标 c 中做到这一点?
    【解决方案6】:

    不幸的是,这些方法都不适合我,所以我自己构建了一个简单的字符串路径,如“element0.element1.element256.element1”等。希望这可以为其他人节省时间。 (只需在字符串中的元素名称之间使用点)

    Json 示例:

    {
        "control": {
            "type": "Button",
            "name": "Save",
            "ui": {
                "scale": 0.5,
                "padding": {
                    "top": 24,
                    "bottom": 32
                }
            }
        }
    }
    

    第一步,将json字符串转成字典

    static func convertToDictionary(text: String) -> [String: Any]? {
            if let data = text.data(using: .utf8) {
                do {
                    return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
                } catch {
                    print(error.localizedDescription)
                }
            }
            return nil
        }
    

    第二步,获取嵌套对象的助手

    //path example: "control.ui.scale"
        static func getDictValue(dict:[String: Any], path:String)->Any?{
            let arr = path.components(separatedBy: ".")
            if(arr.count == 1){
                return dict[String(arr[0])]
            }
            else if (arr.count > 1){
                let p = arr[1...arr.count-1].joined(separator: ".")
                let d = dict[String(arr[0])] as? [String: Any]
                if (d != nil){
                    return getDictValue(dict:d!, path:p)
                }
            }
            return nil
        }
    

    第 3 步,使用助手

    let controlScale = getDictValue(dict:dict, path: "control.ui.scale") as! Double?
    print(controlScale)
    
    let controlName = getDictValue(dict:dict, path: "control.name") as! String?
    print(controlName)
    

    退货

    0.5
    Save
    

    【讨论】:

      【解决方案7】:

      字典的 Swift 4 default: 下标使得更新嵌套字典中的值更加简洁。

      获取和设置默认值而不是处理可选值:

      var dict = [String : [String : String]]()
      dict["deep", default: [:]]["nested"] = "dictionary"
      
      print(dict)
      // ["deep": ["nested": "dictionary"]]
      

      https://swift.org/blog/dictionary-and-set-improvements/

      【讨论】:

        【解决方案8】:

        您可以使用此扩展程序:

        extension Dictionary {
            
            /// - Description
            ///   - The function will return a value on given keypath
            ///   - if Dictionary is ["team": ["name": "KNR"]]  the to fetch team name pass keypath: team.name
            ///   - If you will pass "team" in keypath it will return  team object
            /// - Parameter keyPath: keys joined using '.'  such as "key1.key2.key3"
            func valueForKeyPath <T> (_ keyPath: String) -> T? {
                let array = keyPath.components(separatedBy: ".")
                return value(array, self) as? T
                
            }
            
            /// - Description:"
            ///   - The function will return a value on given keypath. It keep calling recursively until reach to the keypath. Here are few sample:
            ///   - if Dictionary is ["team": ["name": "KNR"]]  the to fetch team name pass keypath: team.name
            ///   - If you will pass "team" in keypath it will return  team object
            /// - Parameters:
            ///   - keys: array of keys in a keypath
            ///   - dictionary: The dictionary in which value need to find
            private func value(_ keys: [String], _ dictionary: Any?) -> Any? {
                guard let dictionary = dictionary as? [String: Any],  !keys.isEmpty else {
                    return nil
                }
                if keys.count == 1 {
                    return dictionary[keys[0]]
                }
                return value(Array(keys.suffix(keys.count - 1)), dictionary[keys[0]])
            }
        }
        

        用法

        let dictionary = ["values" : ["intValue": 3]]
        let value: Int = dictionary.valueForKeyPath("values.intValue")
        

        【讨论】:

          【解决方案9】:

          您可以在 Swift 3/4 上使用以下语法:

          if let name = data["name"] as? String {
              // name has "John"
          }
          
          if let age = data["age"] as? Int {
              // age has 30
          }
          
          if let car = data["cars"] as? [String:AnyObject],
              let car1 = car["car1"] as? String {
              // car1 has "Ford"
          }
          

          【讨论】:

            【解决方案10】:

            使用各种重载字典下标实现的另一种方法:

            let dict = makeDictionary(fromJSONString:
                    """
                    {
                        "control": {
                            "type": "Button",
                            "name": "Save",
                            "ui": {
                                "scale": 0.5,
                                "padding": {
                                    "top": 24,
                                    "bottom": 32
                                }
                            }
                        }
                    }
                    """)!
            
            dict[Int.self, ["control", "ui", "padding", "top"]] // 1
            dict[Int.self, "control", "ui", "padding", "top"]   // 2
            dict[Int.self, "control.ui.padding.top"]        // 3
            

            以及实际的实现:

            extension Dictionary {
                // 1    
                subscript<T>(_ type: T.Type, _ pathKeys: [Key]) -> T? {
                    precondition(pathKeys.count > 0)
            
                    if pathKeys.count == 1 {
                        return self[pathKeys[0]] as? T
                    }
            
                // Drill down to the innermost dictionary accessible through next-to-last key
                    var dict: [Key: Value]? = self
                    for currentKey in pathKeys.dropLast() {
                        dict = dict?[currentKey] as? [Key: Value]
                        if dict == nil {
                            return nil
                        }
                    }
            
                    return dict?[pathKeys.last!] as? T
                }
            
                // 2. Calls 1
                subscript<T>(_ type: T.Type, _ pathKeys: Key...) -> T? {
                    return self[type, pathKeys]
                }
            }
            
            extension Dictionary where Key == String {
                // 3. Calls 1
                subscript<T>(_ type: T.Type, _ keyPath: String) -> T? {
                    return self[type, keyPath.components(separatedBy: ".")]
                }
            }
            
            func makeDictionary(fromJSONString jsonString: String) -> [String: Any]? {
                guard let data = jsonString.data(using: .utf8)
                    else { return nil}
                let ret = try? JSONSerialization.jsonObject(with: data, options: [])
                return ret as? [String: Any]
            }
            

            【讨论】:

              猜你喜欢
              • 2015-11-17
              • 2021-01-03
              • 1970-01-01
              • 1970-01-01
              • 2013-02-11
              • 2021-05-28
              • 2021-12-29
              • 1970-01-01
              • 2016-07-10
              相关资源
              最近更新 更多