rentzsch.com: tales from the red shed

Improving Cocoa/ObjC Enumeration

Papers
I have a number of issues with enumeration in Cocoa/ObjC. Indeed, I have a problem with every one of the three lines necessary in the standard idiom. It even goes beyond that -- I have an problem with the very fact it's three lines of code versus one. Allow me to tear apart the standard Cocoa/ObjC enumeration idiom piece by piece:

NSArray *nameArray = [NSArray arrayWithObjects:@"fred", @"wilma", @"barney", nil];
 
NSEnumerator *enumerator = [nameArray objectEnumerator];
id name;
while( name = [enumerator nextObject] ) {
  NSLog( name ); // do something with name
}

First off, we see the all-to-common ObjC trait of eschewing type information for no good reason. I'd love to be able to say the type information was tossed here just to keep the example code simple. However, experience reading other folks' code doesn't bear this out. Let's fix it up:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name;
while( name = [enumerator nextObject] ) {
  NSLog( name ); // do something with name
}

OK, somewhat better. But for some reason, ObjC coders also like to use assignments within their if statements and while loops, where the compiler is looking for a conditional statement. I've complained about this before, so I won't rehash my argument. There are two ways to fix it:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name = [enumerator nextObject];
while( nil != name ) {
  NSLog( name ); // do something with name
  name = [enumerator nextObject];
}

I dislike this method since it ends up duplicating the code that calls [enumerator nextObject]. I prefer this technique:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name;
while( nil != (name = [enumerator nextObject]) ) {
  NSLog( name ); // do something with name
}

This is about as good as you can get using the stock idioms, but it still has glaring issues. First, it's nonsimple for the common case. You're declaring variables and drafting a nested conditional each time to want to enumerate a collection. Ick.

The first issue of unnecessary complexity leads to the second: you expose both the enumerator (enumerator) and the current value (name) outside the loop that uses it, polluting the namespace. If you have two or more loops in a row, you're now in the unique-name-assignment business:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name;
while( nil != (name = [enumerator nextObject]) ) {
  NSLog( name ); // do something with name
}
 
NSEnumerator *enumerator = [anotherNameArray objectEnumerator];
NSString *name;
while( nil != (name = [enumerator nextObject]) ) {
  NSLog( name ); // do something with name
}
// compiler error: name and enumerator redeclared

Necessary rewrite to avoid the naming collision:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name;
while( nil != (name = [enumerator nextObject]) ) {
  NSLog( name ); // do something with name
}
 
NSEnumerator *anotherEnumerator = [anotherNameArray objectEnumerator];
NSString *anotherName;
while( nil != (anotherName = [anotherEnumerator nextObject]) ) {
  NSLog( anotherName ); // do something with anotherName
}

I recently came off a project that was heavy on enumeration, and these issues were a constant thorn in my side. I finally spent some quality time with the complier, and came up with some preprocessor magic which simplifies enumeration. The best-case standard idiom:

NSEnumerator *enumerator = [nameArray objectEnumerator];
NSString *name;
while( nil != (name = [enumerator nextObject]) ) {
  NSLog( name ); // do something with name
}

reduces to:

nsenumerate( nameArray, NSString, name ) {
  NSLog( name ); // do something with name
}

or, if you want to throw away type-safety:

nsenumerat( nameArray, name ) {
  NSLog( name ); // do something with name
}

Here, name will be an id instead of an NSString*. You can also specify the enumerator yourself:

nsenumerate( [nameArray objectEnumerator], NSString, name ) {
  NSLog( name ); // do something with name
}

But this isn't necessary -- if the object isn't an instance of NSEnumerator or its subclasses, an [objectEnumerator] message will be sent to it. Probably the most common case where you'll want to specify the enumerator is when you want to walk a NSDictionary's keys via [keyEnumerator].

General advantages:

  • Localizes enumeration complexity.
  • Reduces enumeration to one line of code (excluding the #import and work-block, of course).
  • Enumeration-related variables are declared local to the work-block, reducing namespace pollution.
  • Makes it easy to save type information, but also easy to drop when it makes sense.
  • Avoids nongood implicit conditional-from-assignment idiom.

Disadvantages:

  • Not the standard idiom.
  • Because the enumeration-related variables are declared within the for loop's arguments, you must enable GNU89, GNU99, C99 or C++. Xcode's default C89 setting can't handle for-declared variables. (Hint: to switch the language standard setting, get info on the project and look under the "Styles" tab for the "C language standard" setting.)
  • Another Thing to Go Wrong. Indeed, the first version of this code used ObjC++ with templates. That code broke under gcc 3.3. Too bad, since it used the type information to save a runtime call to [isKindOfClass].
  • Yet Another Header Dependency.

Personally, I think the headaches are worth it. I suppose that's obvious, since otherwise I wouldn't have written it...

On an enumeration-related tangent: here's a nice paper (via Lambda the Ultimate) on the differences between enumerators and cursors, arguing for the superiority of enumerators. I whole-heartedly agree, and it's worth pointing out that -- using the terms from the paper -- NSEnumerator is a cursor, not an enumerator.

Update: Michael Tsai comments (with code!).

Sunday, December 07, 2003
12:00 AM