Thursday June 02, 2011

✦ Clash of The Categories

Let’s say we are building an app that needs a special sort order for strings. We wish we had a method on every NSString that we could reference as a comparator. Thanks to the magic of Objective-C categories we can do this:

// In our .m file
@implementation NSString (WithOurStandardCompare)

- (NSComparisonResult)localizedStandardCompare:(NSString *)string
{
    //... Do the magic here
    return comparisonResult;
}

@end

Now, an array of NSStrings can be sorted using our category method like so:

NSArray *sorted = nil;
sorted = [sorted sortedArrayUsingSelector:@selector(localizedStandardCompare:)];

Now The Bad News

In iOS 4.0, Apple added a method on NSString with the exact same name as the one in our fictional category. Whoops. Our method named localizedStandardCompare: is used in place of Apple’s everywhere in our application’s runtime since it’s mixed in as a category after launch.

This works fine for our code that expects our “standard” comparison behavior. But Apple’s new method sorts based on display rules from the Finder. If localizedStandardCompare: is used by any other Apple code expecting the Finder sort behavior, they will get ours instead. You might end up filing a radar report with Apple thinking it’s a fault in their frameworks! Tracking this bug down will take a long, long time.

This illustration may seem a bit contrived, but the problem is very real. If you make use of the sweet libraries available on Cocoa Controls or Github, you could easily find yourself in the middle of a method name collision mess. There’s a lot of good intentions and convenience when using categories, but you must take steps to keep these collisions from happening; especially if you are developing one of these libraries for public reuse!

After talking with a few seasoned devs, I’ve adopted a prefix convention to namespace methods in categories for classes I don’t control. For instance:

// In our .m file
@implementation NSString (WithOurStandardCompare)

- (NSComparisonResult)cm_localizedStandardCompare:(NSString *)string
{
    //... Do the magic here
    return comparisonResult;
}

@end

In this case, I put “cm_” at the beginning of the method name to signify Cocoa Manifest. It’d be an unfortunate miracle if Apple saw the need to prefix their method the same way. And prefixing with initials reasonably defends against collision with other libraries.

Resorting to name prefixes is the norm in Objective-C. Alas, we are stuck with two letter codes like “NS”, “CA”, and our initials to keep these collisions from happening. I’d love to have a better namespace mechanism in the language proper (or use other languages with better idioms—*cough* MacRuby *cough*), but this is what we’re stuck with for now.

If you release libraries in Objective-C with categories on system classes, please, please prefix your method names. I’ve been burned by enough collisions in libraries already.