Swift regret: AnyObject method dispatch— Jordan Rose (@UINT_MIN) August 25, 2021
I bet a lot of people don’t know that you can call methods on AnyObject, as long as they’re ObjC methods. TLDR: I think you shouldn’t have been able to.
But how did that feature end up in the language to begin with?
Part of the Swift Regrets series.
I bet a lot of people don’t know that you can call methods on AnyObject, as long as they’re Objective-C methods (or Swift methods exposed to Objective-C). TLDR: I think you shouldn’t have been able to. But how did that feature end up in the language to begin with?
To answer that, we have to go back to Smalltalk. Smalltalk was a great little language (and programming environment!) designed around the idea that everything’s an object and objects send messages to each other. It wasn’t the first OO language but it was influential. Anyway, one of the neat things about a language like that is that you can send any message to any object, and that object can choose how to respond, possibly dynamically. These days the most well-known language that works like that is probably Ruby.
Objective-C (created in the 80s!) was heavily influenced by Smalltalk, and had the same basic model for its objects. You can send any message to any Objective-C object and it’ll get resolved, possibly at run-time. (Though maybe to an implementation that throws a “does not respond to selector” exception.) The trouble is, Smalltalk and Ruby make everything an object, with the same machine representation (in some form).1 Objective-C…does not, because of C. Integers can be different sizes, floats use alternate registers, and structs have their own set of rules. So to successfully call an Objective-C method, you have to know its parameter and result types, or risk getting garbage.
But ObjC still allowed you to send any message to any object, if the object is typed as
id. How did this work? Well, the compiler looked at all declared methods everywhere (that you’d
#imported) to find one with a matching selector (name). If it couldn’t find one, you got a warning and the compiler would guess at the parameter types and assume the result type was
id. You could get away with that too with just a cast if the actual result was
int, but mostly people would go find the header they needed and import it.
Then ARC came along.
ARC was a codification of the memory management naming conventions that nearly everybody used. In particular, there was a convention that if a method returns an object, by default it’s the caller’s responsibility to retain that object. “It’s valid for now but might not be soon.” For ARC to manage memory for you, it had to be conservative, which meant it had to retain these “+0” return values.2 Which meant it had to be certain about return types.
So that warning for a method call with an unknown selector became an error under ARC. But you can still send any message to
id. It’s just (even) more likely to crash your program if you get it wrong. And this was useful because any sort of general API, like NSArray, used
As I’ve mentioned many times (though not so much on this blog), Swift had to be as good as Objective-C at making most Cocoa apps, or people wouldn’t use it no matter how good the new stuff was. That’s why it’s got so much in the way of Objective-C interop, compared to C (just okay) or C++ (still not supported). So we included this ability to call any known method on AnyObject. We could only support Objective-C methods for this, because Swift methods don’t work by message sending. But even so.
What happened next, though, is that Objective-C got generics and
__kindof, and the number of
id-returning methods in Apple’s SDK dropped precipitously. Meanwhile, AnyObject dispatch was a slow operation to type-check, since it had to map Swift method names back to Objective-C selectors on every class. Doug Gregor and Michael Ilseman finally added lookup tables computed and cached ahead of time. It was also a pain for the old incremental compilation support (since replaced by work from David Ungar, Robert Widmann, and others), since the method could come from any file in your project as well. So the compiler had a hard time with the feature, and the SDK had fewer AnyObjects anyway. And it turns out people didn’t mind casting a little bit more in Swift in those remaining cases.
Whether Swift classes should have used message sends for all methods is a serious question. There’s no doubt it offers impressive flexibility and extensibility in Objective-C (and Ruby). But separate from that, I think we could have gotten away with not implementing AnyObject method dispatch, and telling people to use protocols instead, or make an Objective-C helper function to do the actual call.
@coreload informed me that this was not precisely true in Smalltalk; you could use low-level primitives to control an instance’s memory allocation. But the general point stands: these were not “first-class” values you could pass around. ↩︎
The compiler and runtime teams jumped through hoops to make this less expensive than it sounds. ↩︎