Objective-C is a powerful and extremely useful language, but it's also a bit dangerous. For today's article, my colleague Chris Denter suggested that I talk about pitfalls in Objective-C and Cocoa, inspired by Cay S. Horstmann'sarticle on C++ pitfalls.
Introduction
I'll use the same definition as Horstmann: a pitfall is code that compiles, links, runs, but doesn't do what you might expect it to. He provides this example, which is just as problematic in Objective-C as it is in C++:
if(-0.5<=x<=0.5)return0;
A naive reading of this code would be that it checks to see whetherx is in the range [-0.5, 0.5]. However, that's not the case. Instead, the comparison gets evaluated like this:
if((-0.5<=x)<=0.5)
In C, the value of a comparison expression is anint, either0 or1, a legacy from when C had no built-in boolean type. It is that0 or1, not the value ofx, that is compared with 0.5. In effect, the second comparison works as an extremely weirdly phrased negation operator, such that the if statement's body will execute if and only ifx is less than -0.5.
Nil Comparison
Objective-C is highly unusual in that sending messages tonil does nothing and simply returns0. In nearly every other language you're likely to encounter, the equivalent is either prohibited by the type system or produces a runtime error. This can be both good and bad. Given the subject of the article, we'll concentrate on the bad.
First, let's look at equality testing:
[nilisEqual:@"string"]
Messagingnil returns0, which in this case is equivalent toNO. That happens to be the correct answer here, so we're off to a good start! However, consider this:
[nilisEqual:nil]
Thisalso returnsNO. It doesn't matter that the argument is the exact same value. The argument's value doesn't matterat all, because messages tonil always return0 no matter what. So going byisEqual:,nil never equals anything, including itself. Mostly right, but not always.
Finally, consider one more permutation withnil:
[@"string"isEqual:nil]
What does this do? Well, we can't be sure. It may returnNO. It may throw an exception. It may simply crash. Passingnil to a method that doesn't explicitly say it's allowed is a bad idea, andisEqual: doesn't say that it acceptsnil.
Many Cocoa classes also include acompare: method. This takes another object of the same class and returns eitherNSOrderedAscending,NSOrderedSame, orNSOrderedDescending, to indicate less than, equal, or greater than.
What happens if we compare withnil?
[nilcompare:nil]
This returns0, which happens to be equal toNSOrderedSame. UnlikeisEqual:,compare: thinksnil equalsnil. Handy! However:
[nilcompare:@"string"]
Thisalso returnsNSOrderedSame, which is definitely the wrong answer.compare: will considernil to be equal to anything and everything.
Finally, just likeisEqual:, passingnil as the parameter is a bad idea:
[@"string"compare:nil]
In short, be careful withnil and comparisons. It really just doesn't work right. If there's any chance your code will encounternil, youmust check for and handle it separately before you start doingisEqual: orcompare:.
Hashing
You write a little class to contain some data. You have multiple equivalent instances of this class, so you implementisEqual: so that those instances will be treated as equal. Then you start adding your objects to anNSSet and things start behaving strangely. The set claims to hold multiple objects after you just added one. It can't find stuff you just added. It may even crash or corrupt memory.
This can happen if you implementisEqual: but don't implementhash. A lot of Cocoa code requires that if two objects compare as equal, they will also have the same hash. If you only overrideisEqual:, you violate that requirement. Any time you overrideisEqual:,always overridehash at the same time. For more information, see my article onImplementing Equality and Hashing.
Macros
Imagine you're writing some unit tests. You have a method that's supposed to return an array containing a single object, so you write a test to verify that:
STAssertEqualObjects([objmethod],@[@"expected"],@"Didn't get the expected array");
This uses the new literals syntax to keep things short. Nice, right?
Now we have another method that returnstwo objects, so we write a test for that:
STAssertEqualObjects([objmethodTwo],@[@"expected1",@"expected2"],@"Didn't get the expected array");
Suddenly, the code fails to compile and produces completely bizarre errors. What's going on?
What's going on is thatSTAssertEqualObjects is a macro. Macros are expanded by the preprocessor, and the preprocessor is an ancient and fairly dumb program that doesn't know anything about modern Objective-C syntax, or for that matter modern C syntax. The preprocessor splits macro arguments on commas. It's smart enough to know that parentheses can nest, so this is seen as three arguments:
Macro(a,(b,c),d)
Where the first argument isa, the second is(b, c), and the third isd. However, the preprocessor has no idea that it should do the same thing for[] and{}. With the above macro, the preprocessor seesfour arguments:
[obj methodTwo]@[ @"expected1"@"expected2 ]@"Didn't get the expected array"This results in completely mangled code that not only doesn't compile, but confuses the compiler beyond the ability to provide understandable diagnostics. The solution is easy, once you know what the problem is. Just parenthesize the literal so the preprocessor treats it as one argument:
STAssertEqualObjects([objmethodTwo],(@[@"expected1",@"expected2"]),@"Didn't get the expected array");
Unit tests are where I've run into this most frequently, but it can pop up any time there's a macro. Objective-C literals will fall victim, as will C compound literals. Blocks can also be problematic if you use the comma operator within them, which is rare but legal. You can see that Apple thought about this problem with theirBlock_copy andBlock_release macros in/usr/include/Block.h:
#defineBlock_copy(...)((__typeof(__VA_ARGS__))_Block_copy((constvoid*)(__VA_ARGS__)))#defineBlock_release(...)_Block_release((constvoid*)(__VA_ARGS__))
These macros conceptually take a single argument, but they're declared to take variable arguments to avoid this problem. By taking... and using__VA_ARGS__ to refer to "the argument", multiple "arguments" with commas are reproduced in the macro's output. You can take the same approach to make your own macros safe from this problem, although it only works on the last argument of a multi-argument macro.
Property Synthesis
Take the following class:
@interfaceMyClass :NSObject{NSString*_myIvar;}@property(copy)NSString*myIvar;@end@implementationMyClass@synthesizemyIvar;@end
Nothing wrong with this, right? The ivar declaration and@synthesize are a little redundant in this modern age, but do no harm.
Unfortunately, this code willsilently ignore_myIvar and synthesize anew variable calledmyIvar, without the leading underscore. If you have code that uses the ivar directly, it will see a different value from code that uses the property. Confusion!
The rules for@synthesize variable names are a little weird. If you specify a variable name with@synthesize myIvar = _myIvar;, then of course it uses whatever you specify. If you leave out the variable name, then it synthesizes a variable with the same name as the property. If you leave out@synthesize altogether, then it synthesizes a variable with the same name as the property,but with a leading underscore.
Unless you need to support 32-bit Mac, your best bet these days is to just avoid explicitly declaring backing ivars for properties. Let@synthesize create the variable, and if you get the name wrong, you'll get a nice compiler error instead of mysterious behavior.
Interrupted System Calls
Cocoa code usually sticks to higher level constructs, but sometimes it's useful to drop down a bit and do somePOSIX. For example, this code will write some data to a file descriptor:
intfd;NSData*data=...;constchar*cursor=[databytes];NSUIntegerremaining=[datalength];while(remaining>0){ssize_tresult=write(fd,cursor,remaining);if(result<0){NSLog(@"Failed to write data: %s (%d)",strerror(errno),errno);return;}remaining-=result;cursor+=result;}
However, this can fail, and it will fail strangely and intermittently. POSIX calls like this can be interrupted by signals. Even harmless signals handled elsewhere in the app likeSIGCHLD orSIGINFO can cause this.SIGCHLD can occur if you're usingNSTask or are otherwise working with subprocesses. Whenwrite is interrupted by a signal, it returns-1 and setserrno toEINTR to indicate that the call was interrupted. The above code treats all errors as fatal and will bail out, even though the call just needs to be tried again. The correct code checks for that separately and just retries the call:
while(remaining>0){ssize_tresult=write(fd,cursor,remaining);if(result<0&&errno==EINTR){continue;}elseif(result<0){NSLog(@"Failed to write data: %s (%d)",strerror(errno),errno);return;}remaining-=result;cursor+=result;}
String Lengths
The same string, represented differently, can have different lengths. This is a relatively common but incorrect pattern:
write(fd,[stringUTF8String],[stringlength]);
The problem is thatNSString computes length in terms of UTF-16 code units, whilewrite wants a count of bytes. While the two numbers are equal when the string only contains ASCII (which is why people so frequently get away with writing this incorrect code), they're no longer equal once the string contains non-ASCII characters such as accented characters. Always compute the length of the same representation you're manipulating:
constchar*cStr=[stringUTF8String];write(fd,cStr,strlen(cStr));
Casting to BOOL
Take this bit of code that just checks to see whether an object pointer isnil:
-(BOOL)hasObject{return(BOOL)_object;}
This works... usually. However, roughly 6% of the time, it will returnNO even though_object is notnil. What gives?
TheBOOL type is, unfortunately, not a boolean. Here's how it's defined:
typedefsignedcharBOOL;
This is another bit of unfortunate legacy from the days when C had no boolean type. Cocoa predates C99's_Bool, so it defines its "boolean" type as asigned char, which is just an 8-bit integer. When you cast a pointer to an integer, you just get the numeric value of that pointer. When you cast a pointer to a small integer, you just get the numeric value of the lower bits of that pointer. When the pointer looks like this:
....110011001110000
TheBOOL gets this:
01110000This is not0, meaning that it evaluates as true, so what's the problem? The problem is when the pointer looks like this:
....110011000000000
Then theBOOL gets this:
00000000This is0, also known asNO, even though the pointer wasn'tnil. Oops!
How often does this happen? There are256 possible values in theBOOL, only one of which isNO, so we'd naively expect it to happen about 1/256 of the time. However, Objective-C objects are allocated aligned, normally to16 bytes. This means that the bottom four bits of the pointer are always zero (something thattagged pointers takes advantage of) and there are only four bits of freedom in the resultingBOOL. The odds of getting all zeroes there are about 1/16, or about 6%.
To safely implement this method, perform an explicit comparison againstnil:
-(BOOL)hasObject{return_object!=nil;}
If you want to get clever and unreadable, you can also use the! operator twice. This!! construct is sometimes referred to as C's "convert to boolean" operator, although it's just built from parts:
-(BOOL)hasObject{return!!_object;}
The first! produces1 or0 depending on whether_object isnil, but backwards. The second! then puts it right, resulting in1 if_object is notnil, and0 if it is.
You should probably stick to the!= nil version.
Missing Method Argument
Let's say you're implementing a table view data source. You add this to your class's methods:
-(id)tableView:(NSTableView*)objectValueForTableColumn:(NSTableColumn*)aTableColumnrow:(NSInteger)rowIndex{return[dataArrayobjectAtIndex:rowIndex];}
Then you run your app andNSTableView complains that you haven't implemented this method. But it's right there!
As usual, the computer is correct. The computer is your friend.
Look closer. The first parameter ismissing. Why does this evencompile?
It turns out that Objective-C allows empty selector segments. The above does not declare a method namedtableView:objectValueForTableColumn:row: with a missing argument name. It declares a method namedtableView::row:, and the first argument is namedobjectValueForTableColumn. This is a particularly nasty way to typo the name of a method, and if you do it in a context where the compiler can't warn you about the missing method, you may be trying to debug it for a long time.
Conclusion
Objective-C and Cocoa have plenty of pitfalls ready to trap the unwary programmer. The above is just a sampling. However, it's a good list of things to be careful of.
That's it for today! Check back next time for more wacky advice. Friday Q&A is driven by user ideas, in case you didn't already know, so until next time, pleasesend in your ideas for articles!
- (const char *)UTF8String method ofNSString is that it needs to calculate the number of bytes that the string will take in UTF-8 but there is no way to get that information out of there so you need to callstrlen, which calculates the same information again, again taking a time of O(n).- (const char *)UTF8String returnedBytes:(* int)byteCount would be very helpful. You could then rewrite your example as follows:int byteCount;
const char *cStr = [string UTF8String returnedBytes:&byteCount];
write(fd, cStr, byteCount);
NSMutableArray * result;
[result addObject:someObject];
//... add a bunch of objects
return [NSArray arrayWithArray:result];
-dataUsingEncoding: method is for. It gives you aNSData object which encapsulates both the bytes and the length.i >= 0, and any decent C compiler will warn you that the comparison is always true if you have warnings turned up to decent levels.-dataUsingEncoding as well. How much overhead is there to using the NSData wrapper?if(object) {…? I gather C uses int for the comparison happening in the if. What if pointers were longer than the standard int type, would that be a problem then? Are there any platforms where this is the case?
error: cast from pointer to smaller type 'BOOL' (aka 'signed char') loses information
BOOL x = (BOOL)object;
- (NSString *)newString {
__block NSString *returnString = nil;
dispatch_async(dispatch_get_main_queue(), ^{
returnString = [[NSString alloc] initWithFormat:@"Expensive String"];
});
return returnString;
}
NSMutableArray *result; would give a pointer filled with garbage instead of a valid mutable array object, so adding objects to it and the returning an autoreleased object would fail and most likely crash the app.
if (someObject) {
}
is not a problem anymore.Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.