NSFastEnumeration / NSEnumerator
Enumeration is where computation gets interesting. It’s one thing to encode logic that’s executed once, but applying it across a collection—that’s what makes programming so powerful.
Each programming paradigm has its own way to iterate over a collection:
- Procedural increments a pointer within a loop
- Object Oriented applies a function or block to each object in a collection
- Functional works through a data structure recursively
Objective-C, to echo one of the central themes of this blog, plays a fascinating role as a bridge between the Procedural traditions of C and the Object Oriented model pioneered in Smalltalk. In many ways, enumeration is where the proverbial rubber hits the road.
This article will cover all of the different ways collections are enumerated in Objective-C & Cocoa. How do I love thee? Let me count the ways.
for/while
)
C Loops (for
and while
loops are the “classic” method of iterating over a collection. Anyone who’s taken Computer Science 101 has written code like this before:
for (NSUInteger i = 0; i < [array count]; i++) {
id object = array[i];
NSLog(@"%@", object)
}
But as anyone who has used C-style loops knows, this method is prone to off-by-one errors—particularly when used in a non-standard way.
Fortunately, Smalltalk significantly improved this state of affairs with an idea called list comprehensions, which are commonly known today as for/in
loops.
for/in
)
List Comprehension (By using a higher level of abstraction, declaring the intention of iterating through all elements of a collection, not only are we less prone to error, but there’s a lot less to type:
for (id object in array) {
NSLog(@"%@", object);
}
In Cocoa, comprehensions are available to any class that implements the NSFast
protocol, including NSArray
, NSSet
, and NSDictionary
.
<NSFast Enumeration>
NSFast
contains a single method:
- (NSUInteger)count By Enumerating With State:(NSFast Enumeration State *)state
objects:(id *)stackbuf
count:(NSUInteger)len
state
: Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.stackbuf
: A C array of objects over which the sender is to iterate.len
: The maximum number of objects to return in stackbuf.
One single, deceptively complicated method. There’s that stackbuf
out pointer parameter, and a state
parameter of type NSFast
. Let’s take a closer look at that…
NSFast Enumeration State
typedef struct {
unsigned long state;
id *items Ptr;
unsigned long *mutations Ptr;
unsigned long extra[5];
} NSFast Enumeration State;
state
: Arbitrary state information used by the iterator. Typically this is set to 0 at the beginning of the iteration.items
: A C array of objects.Ptr mutations
: Arbitrary state information used to detect whether the collection has been mutated.Ptr extra
: A C array that you can use to hold returned values.
Under every elegant abstraction is an underlying implementation deserving to be hidden from the eyes of God. items
? mutations
? extra
‽ Seriously, what gives?
For the curious, Mike Ash has a fantastic blog post where he dives into the internals, providing several reference implementations of
NSFast
.Enumeration
What you should know about NSFast
is that it is fast. At least as fast if not significantly faster than rolling your own for
loop, in fact. The secret behind its speed is how -count
buffers collection members, loading them in as necessary. Unlike a single-threaded for
loop implementation, objects can be loaded concurrently, making better use of available system resources.
Apple recommends that you use NSFast
for/in
-style enumeration for your collections wherever possible and appropriate. And to be honest, for how easy it is to use and how well it performs, that’s a pretty easy sell. Seriously, use it.
NSEnumerator
But of course, before NSFast
(circa OS X Leopard / iOS 2.0), there was the venerable NSEnumerator
.
For the uninitiated, NSEnumerator
is an abstract class that implements two methods:
- (id)next Object
- (NSArray *)all Objects
next
returns the next object in the collection, or nil
if unavailable. all
returns all of the remaining objects, if any. NSEnumerator
s can only go forward, and only in single increments.
To enumerate through all elements in a collection, one would use NSEnumerator
thusly:
id object = nil;
NSEnumerator *enumerator = ...;
while ((object = [enumerator next Object])) {
NSLog(@"%@", object);
}
…or because NSEnumerator
itself conforms to <NSFast
in an attempt to stay hip to the way kids do things these days:
for (id object in enumerator) {
NSLog(@"%@", object);
}
If you’re looking for a convenient way to add fast enumeration to your own non-collection-class-backed objects, NSEnumerator
is likely a much more palatable option than getting your hands messy with NSFast
’s implementation details.
Some quick points of interest about NSEnumeration
:
- Reverse an array in one line of code with (and excuse the excessive dot syntax)
array.reverse
.Object Enumerator.all Objects - Add LINQ-style operations with
NSEnumerator
, a third-party library using chainedLinq NSEnumerator
subclasses. - Shake things up with your collection classes in style with
TTTRandomized
, another third-party library, which iterates through elements in a random order.Enumerator
Enumerate With Blocks
Finally, with the introduction of blocks in OS X Snow Leopard / iOS 4, came a new block-based way to enumerate collections:
[array enumerate Objects Using Block:^(id object, NSUInteger idx, BOOL *stop) {
NSLog(@"%@", object);
}];
Collection classes like NSArray
, NSSet
, NSDictionary
, and NSIndex
include a similar set of block enumeration methods.
One of the advantages of this approach is that the current object index (idx
) is passed along with the object itself. The BOOL
pointer allows for early returns, equivalent to a break
statement in a regular C loop.
Unless you actually need the numerical index while iterating, it’s almost always faster to use a for/in
NSFast
loop instead.
One last thing to be aware of are the expanded method variants with an options
parameter:
- (void)enumerate Objects With Options:(NSEnumeration Options)opts
using Block:(void (^)(id obj, NSUInteger idx, BOOL *stop))block
NSEnumeration Options
enum {
NSEnumeration Concurrent = (1UL << 0),
NSEnumeration Reverse = (1UL << 1),
};
typedef NSUInteger NSEnumeration Options;
NSEnumeration
: Specifies that the Block enumeration should be concurrent. The order of invocation is nondeterministic and undefined; this flag is a hint and may be ignored by the implementation under some circumstances; the code of the Block must be safe against concurrent invocation.Concurrent NSEnumeration
: Specifies that the enumeration should be performed in reverse. This option is available forReverse NSArray
andNSIndex
classes; its behavior is undefined forSet NSDictionary
andNSSet
classes, or when combined with theNSEnumeration
flag.Concurrent
Again, fast enumeration is almost certain to be much faster than block enumeration, but these options may be useful if you’re resigned to using blocks.
So there you have all of the conventional forms of enumeration in Objective-C and Cocoa.
What’s especially interesting is that in looking at these approaches, we learn a lesson about the power of abstraction. Higher levels of abstraction are not just easier to write and comprehend, but can often be much faster than doing it the “hard way”.
High-level commands that declare intention, like “iterate through all of the elements of this collection” lend themselves to high-level compiler optimization in a way that just isn’t possible with pointer arithmetic in a loop. Context is a powerful thing, and designing APIs and functionality accordingly ultimately fulfill that great promise of abstraction: to solve larger problems more easily.