Four months ago, I reported a scenario where LLVM would crash with a segmentation fault. A later Xcode version fixed the LLVM crash, but now the crash would occur during runtime. This surprised me:Â Why would it be valid syntax accepted by the compiler, yet crash during execution?
The code in question was:
try! JSONSerialization.data(withJSONObject: !, options: [])
Please ignore the “try” which was necessary to shorten the code to the fewest number of lines without having to add a try/catch. You are looking at the lone exclamation mark. In case you wonder how I ended up with this code: I don’t quite remember if it was a faulty migration to Swift 3 by some earlier Xcode version, or me removing some code by accident.
“Swift 3” is one hint that might put you on the right track. At this version, when the great API renaming took place, Apple replaced many instances of AnyObject with Any. That’s also when we learned that numbers and Strings are no longer automatically counted as objects, but als value types akin to structs. This is also the reason why we had to change to many typed Swift dictionaries and arrays to be using Any.
Nick Lockwood was first in providing the correct explanation to the riddle:
is it possible that it's treating ! as a reference to the "not" function, in the same way you can say array.sort(by: <)
— @nicklockwood@mastodon.social (@nicklockwood) January 1, 2017
AnyObject only accepts objects, but Any accepts everything! Even references to functions. Operator overloading functions only have the operator as their name, and they need no arguments to reference the function itself.
This was later confirmed by Joe from Apple, whose elaborate explanation pleasantly surprised me by e-mail, 9 days later.
Swift considers operators to be regular functions with sugared application syntax. ‘!’ as an expression by itself references the `prefix func !(Bool) -> Bool` in the standard library. That isn’t particularly useful in your code, of course, but you can also do things like ‘array.reduce(0, +)’ to sum the values of an array.
The compiler crashed due to a bug handing function arguments off to ObjC APIs that expect an `id`, though of course NSJSONSerialization doesn’t expect to have a block object in a JSON tree and still fails at runtime.
There you have it. Nick was right all along. 😉
Conclusion
Swift 3 changing AnyObject to Any as the equivalent to id opened the door to a new class of problem that might arise when you are calling the Swift version of something that internally has been implemented in Objective-C.
This change was necessary so that you can still pass strings and number value types Objective-C code which expects an id, like NSArray without lightweight generics to name one example. But this had the somewhat unfortunate side effect that now you can also pass function references as well, whenever the underlying Objective-C code expects an id.
But at least we now know the solution to the initial riddle: ! is a valid reference to a function.
Also published on Medium.
Categories: Q&A