iOS 12 SDK Generic function returns Optional.some(nil)

Viewed 271

Using Xcode 10, but did not migrate to Swift 4.2, so my project is still running with Swift 4.1.

Lets assume i have the following extension on Dictionary:

extension Dictionary where Key: ExpressibleByStringLiteral {
    func find<T>(key: Key) -> T? {
        return self[key] as? T
    }
}

I use this function to access values in a hashmap in a type safe manner like:

 let dict: [String: Any] = ["foo": "bar"]
 let foo: String? = dict.find(key: "foo") // prints "bar"

My problem surfaces, when i would like to have Any type returned from my find function, like:

 let bar: Any? = dict.find(key: "bar")

Pre Xcode 10, this function used to return to me plain and simple nil if the key was not found in the hashmap.

However, post Xcode 10, it returns Optional.some(nil).

I understand, that Any types can be initialised like the following:

let foo: Any = Optional<String>.none

I guess in my case something similar happens. Does anybody has an idea, how to work around it and still return nil from the find function?

1 Answers

This is due to an intentional change (#13910) where the compiler is now more conservative with unwrapping an optional value that's being cast to a generic placeholder type. Now, the results you get are more consistent with those that you would get in a non-generic context (see SR-8704 for further discussion).

For example:

// note the constraint `where Key : ExpressibleByStringLiteral` is needlessly restrictive.
extension Dictionary where Key : ExpressibleByStringLiteral {
  func find<T>(key: Key) -> T? {
    return self[key] as? T
  }
}

let dict: [String: Any] = ["foo": "bar"]

let genericBar: Any? = dict.find(key: "bar")
print(genericBar as Any) // in Swift 4.1: nil, in Swift 4.2: Optional(nil)

// `T` in the above example is inferred to be `Any`.
// Let's therefore substitute `as? T` with `as? Any`.
let nonGenericBar = dict["bar"] as? Any
print(nonGenericBar as Any) // in both versions: Optional(nil)

As you can see, you now get Optional(nil) regardless of whether a generic placeholder was used in order to perform the cast, making the behaviour more consistent.

The reason why you end up with Optional(nil) is because you're performing a conditionally casting an optional value. The conditional cast on its own results in an optional value in order to indicate success or failure, and putting the resulting optional value in the success case gives you a doubly wrapped optional. And because you're casting to Any, which can represent an Optional value, no unwrapping needs to be done in order to "fit" the value in the resultant type.

If you wish to flatten the resulting optional into a singly wrapped optional, you can either coalesce nil:

extension Dictionary {
  func find<T>(key: Key) -> T? {
    return (self[key] as? T?) ?? nil
  }
}

Or unwrap the value before casting, for example using a guard:

extension Dictionary {
  func find<T>(key: Key) -> T? {
    guard let value = self[key] else { return nil }
    return value as? T
  }
}

or, my preferred approach, using flatMap(_:):

extension Dictionary {
  func find<T>(key: Key) -> T? {
    return self[key].flatMap { $0 as? T }
  }
}

That all being said, I often find the usage of [String: Any] to be a code smell, strongly indicating that a stronger type should be used instead. Unless the keys can really be arbitrary strings (rather than a fixed set of statically known keys), and the values can really be of any type for a particular key – there's almost certainly a better type you can be using to model your data with.

Related