Swift `Any? == nil` behavior as a dictionary value

I'm using Moya (or it's Alamofire underlying) to do my network stuff.

I pass my request parameters to the framework as [String: Any], as the framework requires. So when there's a optional value passed, Xcode gives a warning:

Expression implicitly coerced from 'String?' to 'Any'

So I convert the String? to Any, by:

[key: value as Any]

Then strange things happened. In the lldb, I tried to find out if the value is nil, I get this:

(lldb) p parameters[key]
(Any?) $R26 = nil

(lldb) p parameters[key] == nil
(Bool) $R28 = false

(lldb) p print(parameters[key])
Optional(nil)
() $R30 = {}

I mean, it is nil, as it prints out as an none optional string. But it returns false on the equation between it and nil.

But when I try this:

p (parameters[key] as? String?) == nil
(Bool) $R32 = true

It returns true.

Can anybody explain why and how exactly Any and AnyObject functions.

This looks basically correct, but it's hard to see what's going on.

Imagine a dictionary of type [String: String]. Ask for the value of a key:

   let value = parameters[key]

Here, value is of type String?, because the subscript returns nil if the key doesn't exist in the dictionary. If the value does exist, the returned value is that value as an optional, which must be unwrapped.

In other words, the dictionary lookup gives you either Optional<String>.none or Optional<String>.some(aString), where aString is a value you put in the dictionary.

However, your dictionary is more like type [String: String?], because it can contain optional values. Therefore, dictionary lookup gives you:

  • Optional<String?>.none if the value is not in the dictionary, or
  • Optional<String?>.some(Optional<String>.none) if the value is in the dictionary but the value is a nil string, or
  • Optional<String?>.some(Optional<String>.some(aString)) if the value is in the dictionary and is a non-nil string.

Your dictionary is actually [String: Any], which makes the double-level optional harder to see, and lldb isn't very clear about it.

(lldb) p parameters[key]
(Any?) $R26 = nil

There is a value in the dictionary, but the value is a nil string. This is like Optional<String?>.some(Optional<String>.none) in the above list. Looks just like Optional<String?>.none, but it isn't, as the next command shows:

(lldb) p parameters[key] == nil
(Bool) $R28 = false

In that == expression, Swift's behavior is to treat the nil on the RHS like Optional<String?>.none. Since the LHS is actually like Optional<String?>.some(Optional<String>.none), there's no equality.

This is reinforced by the next command:

(lldb) p print(parameters[key])
Optional(nil)
() $R30 = {}

If the key was not in the dictionary, the printed result would be "nil", not "Optional(nil)". That is, printing something like Optional<String?>.none gives "nil", but printing something like Optional<String?>.some(Optional<String>.none) gives "Optional(nil)".

Lastly, but most confusingly:

p (parameters[key] as? String?) == nil
(Bool) $R32 = true

Now things are different, because the type of the LHS is String?? — the extra ? comes from the as? — and type inference forces Swift to treat the RHS as nil as String??. In other words, you're now comparing something like Optional<String?>.some(Optional<String>.none) with itself, which is true!

The moral of the story is: be very careful when putting optional values into a dictionary, and be very, very careful with lldb's attempts to print things "nicely". It may be misleading you.

Note: I had to waffle in my explanation by saying "something like Optional<String?>" because the type is actually Optional<Any?>. However, if you try to rerun the explanation using Any in place of String, it gets incomprehensible, because mere mortals can't really tell Any and Any? apart. The compiler can, though, which is why it all works.

1 Like