简体   繁体   English

如何在 Swift 中访问深层嵌套的字典

[英]How to access deeply nested dictionaries in Swift

I have a pretty complex data structure in my app, which I need to manipulate.我的应用程序中有一个非常复杂的数据结构,我需要对其进行操作。 I am trying to keep track of how many types of bugs a player has in thier garden.我正在尝试跟踪玩家在他们的花园中有多少种错误。 There are ten types of bugs, each with ten patterns, each pattern having ten colors.虫子有十种,每种有十种图案,每种图案有十种颜色。 So there are 1000 unique bugs possible, and I want to track how many of each of these types the player has.所以可能有 1000 个独特的错误,我想跟踪玩家有多少这些类型。 The nested dictionary looks like:嵌套字典如下所示:

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

I do not get any errors or complaints with this syntax.我没有收到任何关于此语法的错误或投诉。

When I want to increment the player's bug collection though, doing this:但是,当我想增加玩家的错误收集时,请执行以下操作:

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

I get this error: String is not convertible to 'DictionaryIndex< String, Any >' with the error's carrot under the first string.我收到此错误: String is not convertible to 'DictionaryIndex< String, Any >' ,错误提示位于第一个字符串下方。

Another similar post suggested using "as Any?"另一个类似的帖子建议使用“as Any?” in the code, but the OP of that post only had a dictionary one deep so could do that easily with: dict["string"] as Any?在代码中,但是那篇文章的 OP 只有一本字典,所以可以很容易地做到这一点: dict["string"] as Any? ... ...

I am not sure how to do this with a multilevel dictionary.我不确定如何使用多级字典执行此操作。 Any help would be appreciated.任何帮助,将不胜感激。

When working with dictionaries you have to remember that a key might not exist in the dictionary.使用字典时,您必须记住字典中可能不存在键。 For this reason, dictionaries always return optionals.出于这个原因,字典总是返回可选项。 So each time you access the dictionary by key you have to unwrap at each level as follows:因此,每次通过键访问字典时,您都必须按如下方式在每个级别解包:

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

I presume you know about optionals, but just to be clear, use the exclamation mark if you are 100% sure the key exists in the dictionary, otherwise it's better to use the question mark:我假设您知道可选,但为了清楚起见,如果您 100% 确定该键存在于字典中,请使用感叹号,否则最好使用问号:

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

Addendum : This is the code I used for testing in playground:附录:这是我用于在操场上进行测试的代码:

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

Another option: You could try calling dict.value( forKeyPath: "ladybug.spotted.red" )!另一种选择:您可以尝试调用dict.value( forKeyPath: "ladybug.spotted.red" )!


So I just tried this with Swift 5:所以我只是用 Swift 5 试过这个:

import Foundation

var d = [ "ladybug" : [ "spotted" : [ "red" : 123 ] ] ] as [String:Any]

(d as NSDictionary).value(forKeyPath: "ladybug.spotted.red")

and it works, but this is probably the best way:它有效,但这可能是最好的方法:

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

I had the same issue, where I wanted to get boolValue nested in dictionary.我有同样的问题,我想让 boolValue 嵌套在字典中。

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

I tried a lot of solution but those didn't worked for me as i was missing type casting.我尝试了很多解决方案,但这些对我不起作用,因为我缺少类型转换。 So I used following code to get the boolValue from json, where json is a nested dictionary of type [String:Any].因此,我使用以下代码从 json 获取 boolValue,其中 json 是 [String:Any] 类型的嵌套字典。

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

My primary use case was reading ad-hoc values from a deep dictionary.我的主要用例是从深度字典中读取临时值。 None of the answers given worked for me in my Swift 3.1 project, so I went looking and found Ole Begemann's excellent extension for Swift dictionaries, with a detailed explanation on how it works.在我的 Swift 3.1 项目中给出的答案都没有对我有用,所以我去寻找并找到了 Ole Begemann 对 Swift 词典的优秀扩展,并详细解释了它是如何工作的。

I've made aGithub gist with the Swift file I made for using it, and I welcome feedback.我用我为使用它而制作的 Swift 文件制作了一个Github 要点,我欢迎反馈。

To use it, you can add the Keypath.swift into your project, and then you can simply use a keyPath subscript syntax on any [String:Any] dictionary as follows.要使用它,您可以将 Keypath.swift 添加到您的项目中,然后您可以简单地在任何[String:Any]字典上使用 keyPath 下标语法,如下所示。

Considering you have a JSON object like so:考虑到您有一个像这样的 JSON 对象:

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

stored in a dictionary var dict:[String:Any] .存储在字典var dict:[String:Any] You could use the following syntax to get to the various depths of the object.您可以使用以下语法来获取对象的各种深度。

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

Note that the extension supports writing into nested dictionaries as well, but I haven't yet used this.请注意,该扩展也支持写入嵌套字典,但我还没有使用它。

I still haven't found a way to access arrays within dictionary objects using this, but it's a start!我仍然没有找到一种方法来使用它访问字典对象中的数组,但这是一个开始! I'm looking for a JSON Pointer implementation for Swift but haven't found one, yet.我正在寻找 Swift 的JSON 指针实现,但还没有找到。

If it's only about retrieval (not manipulation) then here's a Dictionary extension for Swift 3 (code ready for pasting into Xcode playground) :如果它只是关于检索(而不是操作),那么这里是 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"

Unfortunately none of these methods worked for me, so I built my own to use a simple string path like "element0.element1.element256.element1", etc. Hope this save a time for others.不幸的是,这些方法都不适合我,所以我自己构建了一个简单的字符串路径,如“element0.element1.element256.element1”等。希望这可以为其他人节省时间。 (just use a dots between name of elements in string) (只需在字符串中的元素名称之间使用一个点)

Json example: JSON 示例:

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

Step 1, convert json String to Dictionary第一步,将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
    }

Step 2, helper to get a nested objects Step 2, helper 获取嵌套对象

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

Step 3, use helper第三步,使用助手

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)

Returns退货

0.5
Save

The Swift 4 default: subscript for Dictionaries makes makes updating values in nested Dictionaries much more concise. Swift 4 default: subscript for Dictionaries 使得更新嵌套字典中的值更加简洁。

Get and Set a default value rather than dealing with optionals:获取和设置默认值而不是处理选项:

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

print(dict)
// ["deep": ["nested": "dictionary"]]

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

You can use the following syntax on Swift 3/4:您可以在 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"
}

You can use this extension:你可以使用这个扩展:

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

} }

Usage :用法

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

Yet another approach using various overloaded Dictionary subscript implementations:另一种使用各种重载 Dictionary 下标实现的方法:

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

And the actual implementations:以及实际的实现:

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

Yet another Dictionary extension另一个词典扩展

public extension Dictionary where Key: Hashable, Value: Any {
    subscript(keyPath path: String) -> Value? {
        var keys = path.components(separatedBy: ".")
        
        switch keys.first {
        case .some(let key as Key) where keys.count == 1:
            return self[key]
        case .some(let key as Key) where keys.count > 1:
            keys.removeFirst()
            return (self[key] as? Dictionary<Key, Value>)?[keyPath: keys.joined(separator: ".")]
        default:
            return nil
        }
    }
}

Test code:测试代码:

let dict: [String: Any] = [
    "user": [
        "name": "Giorgio",
        "surname": "Baldazzi"
    ]
]

let keyPath = "user.name"
print(String(describing: dict[keyPath: keyPath]))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM