Subscript: access my dictionary values with a Stri

2019-02-11 07:34发布

问题:

I want to do something like that: access my dictionary values with a String enumeration. I am trying to overload the subscript of the dictionary but without success.

Accessing the dictionary:

let district = address[JsonKeys.district]

where JsonKeys is:

enum JsonKeys: String {
    case key1
    case key2
    case key...
}

and my subscript overload is as follow:

extension Dictionary where Key: StringLiteralConvertible, Value: AnyObject {
    subscript(index: FOJsonKeys) -> AnyObject {
        get {
            return self[ index.rawValue] as! AnyObject
        }
    }
}

I get the following message:

**Cannot subscript a value of type 'Dictionary<Key, Value>' with an index of type 'String'**

Where am I wrong?

PS: don't want to do this (this would correct the error, but the code is unreadable this way):

let district = address[JsonKeys.district.rawValue]

The dictionary is a Json parsed dictionary given to me by AlamoFire. I am pretty sure I can't change its type.

回答1:

The simplest approach is to just lift the dictionary into more context. The context in this case is "it only has keys from this enum." Lifting a type in Swift is very straightforward. Just wrap it in a struct.

// This could be a nested type inside JSONObject if you wanted.
enum JSONKeys: String {
    case district
}

// Here's my JSONObject. It's much more type-safe than the dictionary,
// and it's trivial to add methods to it.
struct JSONObject {
    let json: [String: AnyObject]
    init(_ json: [String: AnyObject]) {
        self.json = json
    }

    // You of course could make this generic if you wanted so that it
    // didn't have to be exactly JSONKeys. And of course you could add
    // a setter.
    subscript(key: JSONKeys) -> AnyObject? {
        return json[key.rawValue]
    }
}

let address: [String: AnyObject] = ["district": "Bob"]

// Now it's easy to lift our dictionary into a "JSONObject"
let json = JSONObject(address)

// And you don't even need to include the type. Just the key.
let district = json[.district]


回答2:

Try this:

extension Dictionary where Key: StringLiteralConvertible {
    subscript(index: JsonKeys) -> Value {
        get {
            return self[index.rawValue as! Key]!
        }
    }
}

Remember, with having constraint as Key: StringLiteralConvertible, the extension works for any Dictionaries with its Key conforming to StringLiteralConvertible. (You know many types other than String conform to StringLiteralConvertible.)

To call subscript self[], you need to pass a value of type Key. index.rawValue is String, which may not always be a Key in the extension.

So, the extension I have shown would work for some Dictionaries, would cause runtime crash for some other Dictionaries.


A little bit more type-safe way:

protocol MyJsonKeysConvertible {
    init(jsonKeys: JsonKeys)
}
extension String: MyJsonKeysConvertible {
    init(jsonKeys: JsonKeys) {self = jsonKeys.rawValue}
}
extension Dictionary where Key: MyJsonKeysConvertible {
    subscript(index: JsonKeys) -> Value {
        get {
            return self[Key(jsonKeys: index)]!
        }
    }
}


回答3:

I know that this is an old question, but I'd thought I'd add an implementation that is easier to extend, reuse, and more lightweight

public protocol UsesRawValue {
    var rawValue: String { get }
}

extension JsonKeys: UsesRawValue {}

extension Dictionary where Key: ExpressibleByStringLiteral {
    public subscript(key: UsesRawValue) -> Value? {
        get { return self[key.rawValue as! Key] }
        set { self[key.rawValue as! Key] = newValue }
    }
}

Based on this blog post

This approach only requires us to extend our dictionary once, rather than for each enum. Instead, each enum needs to conform to UsesRawValue. Now we can use it like this.

ajson[JsonKeys.key1]
ajson[JsonKeys.key1] = "name"