Safe Casting in Objective-C
I wrote a small safe casting library for Objective-C. It’s available on GitHub.
The Story
Somewhere on the web I took a quiz to determine how well I know Objective-C. One questioninfuriated me at the time asked something like “if you have an array NSArray *a
, howdo you make it an NSMutableArray
?” The answer was supposedly
NSMutableArray *mutableArray = (NSMutableArray *)a;
But this is tremendously dangerous, wrong, crazy… The real answer is something like
NSMutableArray *mutableArray = [a mutableCopy];
Unless you want to actually modify the same instance that a
is if it’s a mutable array,in which case you have to check whether it’s mutable.
NSMutableArray *mutableArray; if ([a isKindOfClass:[NSMutableArray class]]) { mutableArray = a; }
At which point you proceed to mutate mutableArray
, which is also a
, possibly ignoring thefact that mutableArray
may be nil
. Or maybe you return early if it’s nil
.
Casting is Dangerous
If you cast between object that are the same foundational type, no work is done. What Imean is that if you cast from an float to an int, since they’re represented differently inthe machine, some work is done to represent that float as a int that changes the valueyou’re working with.
In Objective-C, when we’re dealing with objects, we’re dealing with pointers to objects.These are really just pointers. They represent a memory address, not a real usablevalue. So if you cast an NSArray
to an NSMutableArray
, nothing happens. Same if youcase an object to an int *
. (Which is coincidentally prohibited by ARC. Before ARC,casting an object to an int *
used to be perfectly valid, if usually insane.)
Almost every time I see a cast involving an Objective-C object, I get very worried.Objective-C has a type system and a compiler that does its best to enforce correct types.We should use it. Our compiler helps us move finding bugs from run time to compile time.Casting is a way to tell the compiler “don’t worry, I know what I’m doing.” But I’ve seentoo many cases of people not knowing what they’re doing. They forget that methodsignatures specify a more general type for a reason. If you are passed an object with type
, you shouldn’t even assume you’re working with an NSObject. And if you’repassing around raw id
s all the time, you should strongly consider coming up with goodprotocols. Or maybe you’re taking a parameter that may be one of two very distinct typesyou say? Well that’s certainly odd, but I hope you’re at least doing the Objective-Ccasting dance.
Getting Rid of the Casting Dance
I wrote the casting dance out above. But here it is again with a little more detail.
NSMutableArray *ma; if ([a isKindOfClass:[NSMutableArray class]]) { // You have a mutable array! ma = a; } else { // You don't have a mutable array! } // `ma` is `nil` if `a` wasn't originally an `NSMutableArray`
I want to get rid of that, and many things like it. So this project aims to reduce theabove down to this
NSMutableArray *ma = [NSMutableArray cast:a]; // `ma` is `nil` if `a` wasn't originally an `NSMutableArray`
Conditional Code
Often what we really want to do is run certain code if an object is of a specific type.
if ([a isKindOfClass:[NSMutableArray class]]) { NSMutableArray *ma = a; [ma addObject:x]; }
Technically, this could have the same effect
[[NSMutableArray cast:a] addObject:x];
Practically, this ends up getting more complex. Likely I will want to include ways of running a block with the passed object if the object is the kind that’s expected. Maybe something like
[NSMutableArray cast:a intoBlock:^(NSMutableArray *ma){ [ma addObject:x]; [ma addObject:y]; [ma addObject:z]; }];
or even
BOOL success = [NSMutableArray cast:a intoBlock:^(NSMutableArray *ma){ [ma addObject:x]; [ma addObject:y]; [ma addObject:z]; }]; if (!success){ // Do whatever it is you need to do if `a` is not mutable... }
But what’s the point?
Conciseness
It is more concise, while also being idiomatic.
Education
Every Objective-C programmer knows how to cast. Sadly, many don’t understand that itdoesn’t give you any guarantees. It just tells the compiler “shut up, I know what I’mdoing”. Here’s at least one example more related to fast enumeration.But surely you’ve explained this sort of thing to somebody in the past? It’s an honestenough mistake to make.
It’s too easy to gloss over the casting dance, correct code that looks very familiar (andif and a cast), and not realize that there’s a very important technical decisionbeing made there. I would rather somebody come across a weird cast: method in my code baseand ask why it’s there. Then I can go on about the dangers of casting.
Clarity
The closer that code can read to prose the better the intent of the programmer can beunderstood. It also means that there’s fewer places for bugs to creep in. Consider doingsomething with items in an array.
NSArray *a; for (int i = 0; i < a.count; i++) { NSLog(@"%@", a[i]); }
Then fast enumeration came along, and off-by-one and out-of-bounds errors became rarer.
NSArray *a; for (id object in a) { NSLog(@"%@", object); }
And I even prefer block enumeration sometimes.
NSArray *a; [a enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSLog(@"%@", a); }];
This is especially helpful when dealing with other collection types. Some people don’tknow off the top of their heads whether fast enumeration of a dictionary iterates overobjects or keys.
Digress and Reflect
With all the recent chatter about Apple needing to replace Objective-C,one common call is to move away from C. That “we’re one NULL pointer dereference awayfrom a crash”. I don’t remember the last time code I’ve worked closely with crashed froma NULL pointer dereference. It happens when you’re using C language features, and mostcode where you can’t avoid C is code that belongs in C (image manipulation, DSP).I think it’s clear that the very best Objective-C developers need to know and love Cinside and out. But you can make a lot of pretty solid apps without knowing much about C.
I think it’s much more common for an app to crash with unrecognized selector sentto instance. And here’s why I brought up the “Apple needs to drop Objective-C” thing.Objective-C is remarkable for its powers of introspection and runtime manipulation.