Tuesday, October 20, 2009

Objective-C Tuesdays: The for...in loop

We've looked at the standard for loop for the last two weeks. This week we will dive into the for...in loop introduced in Objective-C 2.0. Unlike the standard for loop, the for...in loop is not available in plain old C.

In Objective-C, collection classes such as NSArray, NSSet and NSDictionary are a key part of the Objective-C Foundation framework. Collections provide high level ways to group and organize objects.

In older versions of Objective-C, looping over items in a collection is done using an NSEnumerator object in a while loop:
NSSet *items = [NSSet setWithObjects:@"foo", @"bar", nil];
NSEnumerator *enumerator = [items objectEnumerator];
NSString *item = nil;
while (item = [enumerator nextObject]) {
  // do something with item
}
It's possible to loop over items in a NSArray using a standard for loop, since NSArray has a well defined order:
NSArray *items = [NSArray arrayWithObjects:@"foo", @"bar", nil];
for (NSUInteger i = 0; i < [items count]; i++) {
  NSString *item = [items objectAtIndex:i];
  // do something with item
}
Unfortunately, some collection classes (such as NSSet) don't have a well defined order; NSEnumerator used to be the only option.

The for...in loop works on any collection that conforms to the NSFastEnumeration protocol (all the standard ones do). The for...in loop is similar to a standard for loop but simpler. Instead of the three sections of a standard for loop, there are two, the loop variable and the collection expression:
for (loop variable in collection expression) {
  // do something with loop variable
}
Loop Variable The loop variable can be a previous declared variable:
NSString *item = nil;
// ...
for (item in collection expression) {
  // do something with item
}
or it can be declared inside the parentheses:
for (NSString *item in collection expression) {
  // do something with item
}
Collection Expression The collection expression can be any expression that evaluates to an object conforming to NSFastEnumeration. Typically, this is simply a collection variable defined elsewhere:
NSSet *items = [NSSet setWithObjects:@"foo", @"bar", nil];
// ...
for (NSString *item in items) {
  // do something with item
}
but can be a function call or method call:
for (NSString *item in [NSSet setWithObjects:@"foo", @"bar", nil]) {
  // do something with item
}
Dictionaries When using a for...in loop with a NSDictionary, the loop variable receives the dictionary keys; to work with the dictionary values inside the loop, use objectForKey:
NSDictionary *numbers = [NSDictionary dictionaryWithObjectsAndKeys:
    @"zero", @"0", 
    @"one", @"1", 
    nil];
for (NSString *key in numbers) {
  NSString *value = [numbers objectForKey:key];
  // do something with key and value
}
Mutation Guard Modifying a collection while iterating over it can cause very unintuitive behavior, so the for...in loop uses a mutation guard behind the scenes. If items are added or removed from a collection while your for...in loop is running, an exception is thrown. This is generally a good thing, but it makes filtering a collection somewhat tricky:
NSMutableSet *items = [NSMutableSet setWithObjects:@"", @"a", @"aa", @"aaa", nil];
for (NSString *item in items) {
  if (item.length < 2) {
    [items removeObject:item]; // WARNING: exception thrown on mutation
  }
}
The way to get around this restriction is to iterate over a copy of the collection and modify the original (or vice versa):
NSMutableSet *items = [NSMutableSet setWithObjects:@"", @"a", @"aa", @"aaa", nil];
for (NSString *item in [[items copy] autorelease]) {
  if (item.length < 2) {
    [items removeObject:item]; // OKAY: looping over copy, changing original
  }
}
Next week, we will look at the while loop.

4 comments:

Dax said...

Using -copy in that way is a memory leak. You need to autorelease it, or release the copy when you're done with it.

Don McCaughey said...

Doh! You're totally right. I'll fix up the code to autorelease in place.

godDLL said...

I think Apple's engineers put this safeguard there for a reason. The reason being – more often than not going over a collection of objects in a loop leads to memory issues.
If you need to do that, then I think of this exception throwing business as a gentle nudge to re-think your architecture.

Don McCaughey said...

That's certainly true. But even when memory management is automatic as in Java (or Cocoa on Mac), changing a collection while iterating over it has a similar mutation guard. I think this is due to the difficulty in implementing the correct behavior. For instance, suppose you loop over an NSArray and on the first iteration, remove the first item and append it to the end. You would expect that you would get the original second item (now the first item) on the second iteration, and that when you reached the end, you would skip the last item (which was originally the first item). Think about how to implement this and it starts to get complicated fast. Much cleaner to just say "don't change the collection while looping over it."