Tuesday, November 24, 2009

Objective-C Tuesdays: continue

Last week we looked at how to end a loop early using the break keyword. Today we will look at a similar action: how to skip to the next iteration.

Sometimes you need to process each item in a collection or sequence, but some of those items get full processing and some don't. For example, you may want to skip empty strings in a collection. Frequently you do this using an if...else statement:
NSArray *collection = [NSArray arrayWithObjects: @"foo", @"", @"bar", @"baz", @"", nil];
int wordCount = 0;
for (NSString *item in collection) {
  NSLog(@"found '%@'", item);
  if (item.length > 0) {
    wordCount++;
  }
}
NSLog(@"word count = %d", wordCount);
This is generally a good approach, but sometimes you have complex nested logic:
NSArray *collection = [NSArray arrayWithObjects: @"foo", @"\n", @"bar", @"baz", @"", nil];
int wordCount = 0;
for (NSString *item in collection) {
  NSLog(@"found '%@'", item);
  if (item.length > 0) {
    if ( ! [item isEqualToString:@"\n"]) {
      wordCount++;
      if (item.length < 4) {
        NSLog(@"short word");
      } else {
        NSLog(@"long word");
      }
    }
  }
}
NSLog(@"word count = %d", wordCount);
The continue statement can help you simplify complicated cases like this by stopping the execution of the loop body for the current item and advancing to the next. Using continue, we can rewrite the example like this:
NSArray *collection = [NSArray arrayWithObjects: @"foo", @"\n", @"bar", @"baz", @"", nil];
int wordCount = 0;
for (NSString *item in collection) {
  NSLog(@"found '%@'", item);
  if (item.length > 0) continue;
  if ([@item isEqualToString:"\n"]) continue;

  wordCount++;
  if (item.length < 4) {
    NSLog(@"short word");
  } else {
    NSLog(@"long word");
  }
}
NSLog(@"word count = %d", wordCount);
Like break, a continue statement only works on the innermost loop that encloses it:
// outer loop
for (int i = 0; i < 10; i++) { // loop A
  if (...) continue; // skips to next item in A
  
  // inner loop
  for (int j = 0; j < 10; j++) { // loop B
    if (...) continue; // skips to next item in B
  }
  
  if (...) continue; // skips to next item in A
  
}
The continue statement is most useful with a for or for...in loop, but can be used with a while and do...while loop with care. It's easy to create an infinite while loop using continue:
NSArray *collection = [NSArray arrayWithObjects: @"foo", @"", @"bar", @"baz", @"", nil];
int wordCount = 0;
int i = 0;
while (i < collection.count) {
  NSString *item = [collection objectAtIndex:i];
  NSLog(@"found '%@'", item);
  if (item.length > 0) {
    continue; // OOPS! forgot to increment i
  }

  wordCount++;
  i++;
}
NSLog(@"word count = %d", wordCount);
This loop will reach the second item in the collection and get stuck there -- it never reaches the i++ at the end of the loop body. The solution is simple:
NSArray *collection = [NSArray arrayWithObjects: @"foo", @"", @"bar", @"baz", @"", nil];
int wordCount = 0;
int i = 0;
while (i < collection.count) {
  NSString *item = [collection objectAtIndex:i];
  NSLog(@"found '%@'", item);
  if (item.length > 0) {
    i++;;  // move to next item
    continue;
  }

  wordCount++;
  i++;
}
NSLog(@"word count = %d", wordCount);
This is a consequence of the free-form nature of the while and do...while loops. The compiler knows how to make a for or for...in loop advance to the next item, but the other loops leave that up to you; continue acts more like a special goto statement with while and do...while loops.

Next time, we'll look at the mother of all loops, the goto statement.

3 comments:

Dave Proffer said...

I believe your 2nd example code block in this post has two errors, 1 logic and 1 syntax:

If (item.length > 0 ) continue;
// should be
if (item.length == 0) continue;
// and
if ([@item isEqualToString:"\n"]) continue;
// should be
if ([item isEqualToString:@"\n"]) continue;

Don McCaughey said...

Thanks Dave! I fixed the syntax error in the second example, the @ is now with the "\n" where it belongs. The if statement now reads:

if ( ! [item isEqualToString:@"\n"]) { ...

I think the first if statement should still be:

if (item.length > 0) { ...

The purpose of the if statement is to exclude empty strings. It could alternately be written as:

if ( ! [item isEqualToString:@""]) { ...

I hope you enjoyed the post otherwise.

Nekura said...

Great article! I believe, however, that what Dave was referring to by:

If (item.length > 0 ) continue;
// should be
if (item.length == 0) continue;

was in your 3rd code block. As it is, an item of length greater than 0 would skip (continue) the rest of the loop, when you really want the opposite effect.