Checking for nil Value in Swift Dictionary Extensi

2019-01-15 09:58发布

问题:

I have the following code in a playground file:

extension Dictionary {

    func test()  {

        for key in self.keys {
            self[key]
        }
    }
}

var dict: [String: AnyObject?] = [
    "test": nil
]

dict.test()

I will henceforth refer to the line within the for-each loop as the output since it is what's relevant. In this particular instance the output is nil.

When I change the for-each loop to look like this:

for key in self.keys {
    print(self[key])
}

The output is "Optional(nil)\n".

What I really want to do is check the value for nil, but the code:

for key in self.keys {
    self[key] == nil
}

outputs false.

One other thing I tried was the following:

for key in self.keys {
    self[key] as! AnyObject? == nil
}

which produces the error:

Could not cast value of type 'Swift.Optional<Swift.AnyObject>' to 'Swift.AnyObject'

Any help with this is much appreciated!

回答1:

You've gotten yourself into kind a mess, because a dictionary whose values can be nil presents you with the prospect of a double-wrapped Optional, and therefore two kinds of nil. There is the nil that you get if the key is missing, and then the nil that you get if the key is not missing and you unwrap the fetched result. And unfortunately, you're testing in a playground, which is a poor venue for exploring the distinction.

To see what I mean, consider just the following:

var d : [String:Int?] = ["Matt":1]
let val = d["Matt"]

What is the type of val? It's Int?? - an Int wrapped in an Optional wrapped in another Optional. That's because the value inside the dictionary was, by definition, an Int wrapped in an Optional, and then fetching the value by its key wraps that in another Optional.

So now let's go a bit further and do it this way:

var d : [String:Int?] = ["Matt":nil]
let val = d["Matt"]

What is val? Well, the playground may say it is nil, but the truth is more complicated; it's nil wrapped in another Optional. That is easiest to see if you print val, in which case you get, not nil, but "Optional(nil)".

But if we try for something where the key isn't there at all, we get a different answer:

let val2 = d["Alex"]

That really is nil, signifying that the key is missing. And if we print val2, we get "nil". The playground fails to make the distinction (it says nil for both val and val2), but converting to a String (which is what print does) shows the difference.

So part of the problem is this whole double-wrapped Optional thing, and the other part is that the Playground represents a double-wrapped Optional in a very misleading way.

MORAL 1: A dictionary whose value type is an Optional can get really tricky.

MORAL 2: Playgrounds are the work of the devil. Avoid them. Use a real app, and use logging to the console, or the variables pane of the debugger, to see what's really happening.



回答2:

Checking if a dictionary value is nil makes sense only if the dictionary values are an Optional type, which means that you have to restrict your extension method to that case.

This is possible by defining a protocol that all optional types conform to (compare How can I write a function that will unwrap a generic property in swift assuming it is an optional type?, which is just a slight modification of Creating an extension to filter nils from an Array in Swift):

protocol OptionalType {  
    typealias Wrapped 
    var asOptional : Wrapped? { get }
}  

extension Optional : OptionalType {  
    var asOptional : Wrapped? {  
        return self 
    }  
}

Now you can define an extension method with is restricted to dictionaries of optional value types:

extension Dictionary where Value : OptionalType {

    func test()  {
        for key in self.keys { ... }
    }
}

As Matt already explained, self[key] can only be nil if that key is missing in the dictionary, and that can not happen here. So you can always retrieve the value for that key

            let value = self[key]! 

inside that loop. Better, enumerate over keys and values:

        for (key, value) in self { ... }

Now value is the dictionary value (for key), and its type conforms to OptionalType. Using the asOptional protocol property you get an optional which can be tested against nil:

             if value.asOptional == nil { ... }

or used with optional binding.

Putting all that together:

extension Dictionary where Value : OptionalType {

    func test()  {
        for (key, value) in self {
            if let unwrappedValue = value.asOptional {
                print("Unwrapped value for '\(key)' is '\(unwrappedValue)'")
            } else {
                print("Value for '\(key)' is nil")
            }
        }
    }
}

Example:

var dict: [String: AnyObject?] = [
    "foo" : "bar",
    "test": nil
]

dict.test()

Output:

Value for 'test' is nil
Unwrapped value for 'foo' is 'bar'