Swift: Any can bite

Any and Any? in Swift are weird. We venture forth into the depth of unknown using Swift Reflection API as the basis for our research and find out some inconsistent behaviors for Swift casting API, when working with Any.

Recently, when playing around with Swift Reflection API, I stumbled upon a painful problem. Basically, when typecasting Any and Any? to protocols Swift just couldn’t help it and started screwing me in all my glory.

Without the further ado, here is the code, that ate me, raped me and killed what was left (in that specific order, mind you):

You can read more about the cast function in my previous Type Inference in Swift blog post.

If we run the code for nested property, the results would be as expected:

The fun part starts, when we try to run the printCasts with optionalNested property:

The results of the call are as weird as T-Rex trying to put my hat on while hiding in my wardrobe:

Swift managed to unwrap the Optional<optional> into nested, failed to do so, when unwrapping to Greetable protocol, to which Nested conforms, but managed to do the double optional unwrapping to protocol, when we’ve done an intermediate cast to AnyObject.

That is really bad at the most basic level, as we can’t use the same function for handling different values hidden by Any and we don’t have any non explicit way of knowing, what’s in there.

Please note, that the irregular behavior in here is the unwrapping of double optional. Value for optionalNested key is optional by itself, as the property is optional, and it is wrapped in another optional, when first? is called. For optionalNested none of the three conditions should have worked. This point is valid for explicit unwrapping from Type?? to Type, where Type != Any:

This results in compiler error: downcast from 'Int??' to 'Int' only unwraps optionals.
I tried playing around and found another interesting behavior:

Prints 1. So, it seems, like the compiler automatically unwraps, when casting to Any? and then if let casted to Type results in another unwrap.

So, how should we get around that?
We could try doing the cast to Optional<optional>:

Sadly, that won’t do, as the compiler wouldn’t even consider compiling the thing. It would just return an error instead: error: cannot downcast from 'Any?' to a more optional type 'Greetable??'
That creeped me out, as Any could be of any type, meaning, that it could be an Optional, other wrapper (e.g. Either, Result, etc.) under the hood. Seems, like Any is just not defined well enough at that point in space and time (just imagine how lucky someone is in a parallel universe, where swiftc is well-written, doesn’t lag or crash).

Casting to AnyObject or Any is a hack and we should of course avoid that, as we could never be sure as to why it actually works and if future compiler versions would break it.
A better(???) way would be to override the compiler checks and cast to appropriate types. We could do that by using the generic casting function:

This yields us better results in terms of types, but Nested is still being unwrapped from double optionals.

But the best solution is to actually avoid Any? at all costs and be as explicit, as possible. Build your code around generics instead. On the other hand, there are not that much Swift APIs return the result typed as Any, which could be both Type and Optional under the hood. So, if you use them, think in terms of proper language behavior (even, if this results in bloating your code). Don’t forget to cover everything Any-related with tests. I mean it, 100% test coverage of your code, library code, kid(s), wife(s)/husband(s), dog(s), cat(s), hamster(s), house(s), car(s), parent(s) and anything else you treasure is a must, as you never know, when the compiler would change it’s behavior. And don’t even try saying I didn’t warn you.

For my case, I just ignore the type system from the Swift Reflection API (namely, Mirror) based on Any and pretend I’m using ordinary possible values from my properties. Moreover, I explicitly unwrap the Optional to Any before casting just in case. This lets me avoid implicit double optional unwrapping and feel myself safe and secure (although I’m unsure this actually helps, as betterPrintCasts works the same way):

results in the following output for optionalNested and nested property:

That’s all, folks. Have a great day and stay DRY, no matter, where you are.

Comments