Effective Objective-C 2.0:Item 48: Prefer Block Enumeration to for Loops

来源:互联网 发布:淘宝远程钓鱼软件 编辑:程序博客网 时间:2024/06/04 18:38

Item 48: Prefer Block Enumeration to for Loops

Enumerating a collection is a very common task in programming, and modern Objective-C has many ways to do so, ranging from standard C loops to NSEnumerator in Objective-C 1.0 and fast enumeration in Objective-C 2.0. The addition of blocks (see Item 37) to the language brought a few new methods that developers sometimes overlooked. These methods allow you to enumerate a collection by passing a block that should be run for each item in the collection and are usually much easier to use, as I will explain.

The collections presented in this item—NSArrayNSDictionary, andNSSet—are the ones most commonly used. Additionally, custom collections can all be made to support any of the enumeration techniques outlined, although explaining how is beyond the scope of this item.

For Loops

The first method for enumerating a collection is the good, old-fashioned forloop, which harks back to Objective-C’s roots in the C language. This method is very basic and therefore quite limiting. The usual idea is to do something like this:

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
    id object = anArray[i];
    // Do something with 'object'
}

This is acceptable but becomes more complicated for a dictionary or a set:

// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
    id key = keys[i];
    id value = aDictionary[key];
    // Do something with 'key' and 'value'
}

// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
    id object = objects[i];
    // Do something with 'object'
}

By definition, dictionaries and sets are unordered, so there’s no way to directly access the value at a certain integer index. Therefore, you need to ask for all the keys for the dictionary or all the objects for a set; in both cases, the ordered array returned can then be enumerated instead to access the values. Creating this extra array is extra work and causes an extra object to be created that retains the objects in the collection. Of course, those objects will be released when the array is released, but it’s more unnecessary method calls. All the other enumeration techniques mitigate needing to create an extra intermediate array.

Enumerating backward can be achieved with a for loop by starting at the count of objects minus one, decrementing the counter on each iteration, and stopping when the counter equals zero. This is much easier.

Objective-C 1.0 Enumeration Using NSEnumerator

The NSEnumerator object is an abstract base class that defines only two methods for concrete subclasses to implement:

- (NSArray*)allObjects
- (id)nextObject

The key method is nextObject, which returns the next object in the enumeration. Each time the method is invoked, internal data structures are updated such that the next invocation returns the next object. After all the objects in the enumeration have been returned, nil is returned, signaling the end of enumeration.

All the built-in collection classes within the Foundation framework implement enumeration in this way. For example, enumerating an array is done like this:

NSArray *anArray = /* ... */;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
}

This is similar to a standard for loop but is more workThe only real benefit is that enumerating any collection is made with similar syntax. For example, consider the equivalent for a dictionary and a set:

// Dictionary
NSDictionary *aDictionary = /* ... */;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
    id value = aDictionary[key];
    // Do something with 'key' and 'value'
}

// Set
NSSet *aSet = /* ... */;
NSEnumerator *enumerator = [aSet objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
}

The dictionary enumeration is slightly different because a dictionary has keys and values, so the value has to be pulled out of the dictionary given the key. The additional benefit to NSEnumerator is that different types of enumerators are often available. For example, with an array, there is a reverse enumerator; if that is used instead, the collection is iterated in reverse. For example:

NSArray *anArray = /* ... */;
NSEnumerator *enumerator = [anArray reverseObjectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
}

This is much easier to read than the equivalent syntax of reverse enumerating using a for loop.

Fast Enumeration

Fast enumeration was introduced with Objective-C 2.0. Fast enumeration is similar to enumeration using NSEnumerator, except that the syntax is much more condensed, adding a keyword, in, to the for-loop syntax. This keyword greatly condenses the syntax for enumerating a collection, such as an array:

NSArray *anArray = /* ... */;
for (id object in anArray) {
    // Do something with 'object'
}

This is much simpler! It works by using a protocol calledNSFastEnumeration, to which an object can conform in order to indicate that it supports fast enumeration. The protocol defines a single method:

- (NSUInteger)countByEnumeratingWithState:
                            (NSFastEnumerationState*)state
                                  objects:(id*)stackbuffer
                                    count:(NSUInteger)length

Explaining how this works in full is beyond the scope of this item. However, decent tutorials on the Internet explain this topic well. The important thing to note is that this method allows the class to return multiple objects at the same time, which means that the enumeration loop can be more efficient.

Enumerating dictionaries and sets is just as simple:

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
    id value = aDictionary[key];
    // Do something with 'key' and 'value'
}

// Set
NSSet *aSet = /* ... */;
for (id object in aSet) {
    // Do something with 'object'
}

Reverse enumeration can be achieved by noting that NSEnumerator objects also implement NSFastEnumeration. So to reverse iterate through an array, you can do the following:

NSArray *anArray = /* ... */;
for (id object in [anArray reverseObjectEnumerator]) {
    // Do something with 'object'
}

This method of enumeration is the best so far in terms of syntax and efficiency, but enumerating a dictionary still requires an additional step if you want both the key and the value. Also, the index of enumeration is not easily accessible, unlike a traditional for loop. The index is often useful to have during an iteration, as many algorithms will make use of it.

Block-Based Enumeration

Block-based methods are the final type of enumeration available in modern Objective-C. The most basic method for enumerating an array is as follows, defined on NSArray:

- (void)enumerateObjectsUsingBlock:
           (void(^)(id object, NSUInteger idx, BOOL *stop))block

The other methods in this family can take options to control the enumeration and are discussed later.

For the array and the set, the block that is performed for each iteration takes the object at that iteration, the index into the iteration, and a pointer to a Boolean. The first two parameters are self-explanatory. The third provides a mechanism for halting the enumeration.

For example, you can enumerate an array with this method as follows:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:
    ^(id object, NSUInteger idx, BOOL *stop){
        // Do something with 'object'
        if (shouldStop) {
            *stop = YES;
        }
    }];

This syntax is slightly more verbose than fast enumeration but is clean, and you get both the index of iteration and the object. This method also provides a clean way to stop the enumeration, if you wish, through thestop variable, although a break can achieve the same thing in the other methods of enumeration and is just as clean.

It’s not just arrays that can be enumerated in this way. The same block-enumeration method exists in NSSet and a slightly different one inNSDictionary:

- (void)enumerateKeysAndObjectsUsingBlock:
                    (void(^)(id key, id object, BOOL *stop))block

Therefore, enumerating dictionaries and sets is just as simple:

// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:
    ^(id key, id object, BOOL *stop){
        // Do something with 'key' and 'object'
        if (shouldStop) {
            *stop = YES;
        }
    }];

// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:
    ^(id object, BOOL *stop){
        // Do something with 'object'
        if (shouldStop) {
            *stop = YES;
        }
    }];

The big win here is that you get a lot more information directly in the block. In the case of arrays, you get the index of enumeration. The same goes for ordered sets (NSOrderedSet). In the case of a dictionary, you get both the key and the value without any additional work, thereby saving the extra cycles required to obtain the value for a given key. Instead, the dictionary can give both at the same time, which is highly likely to be far more efficient, since keys and values will be stored together in a dictionary’s internal data structures.

Another benefit is that you can change the method signature of the block to limit the need for casting; in effect, you push the cast into the block-method signature. Consider the code for enumerating a dictionary with fast enumeration. If the objects in the dictionary are known to you as strings, you may do this:

for (NSString *key in aDictionary) {
    NSString *object = (NSString*)aDictionary[key];
    // Do something with 'key' and 'object'
}

With the block-based method, you can do the cast in the block-method signature like so:

NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:
    ^(NSString *key, NSString *obj, BOOL *stop){
        // Do something with 'key' and 'obj'
    }];

This works because the id type is rather special and can be overridden like this. If the original block signature had defined key and object asNSObject*, you wouldn’t be able to do this trick. This technique is more useful than it seems at first glance. Giving the exact type of an object allows the compiler to help you by throwing an error if a method you call on the object doesn’t exist. If you can guarantee what type of objects are in a certain collection, indicating the type in this way should always be done.

The ability to reverse enumerate is not lost. Arrays, dictionaries, and sets all implement a variant of the preceding method, allowing you to pass an options mask:

- (void)enumerateObjectsWithOptions:
              (NSEnumerationOptions)options
                         usingBlock:
              (void(^)(id obj, NSUInteger idx, BOOL *stop))block
- (void)enumerateKeysAndObjectsWithOptions:
              (NSEnumerationOptions)options
                                usingBlock:
              (void(^)(id key, id obj, BOOL *stop))block

The NSEnumerationOptions type is an enum whose values you can bitwise OR together to indicate how the enumeration should behave. For example, you can request that iteration be concurrent, meaning that the blocks for each iteration will be executed in parallel if that is possible with current system resources. This is achieved through the NSEnumerationConcurrentoption. Underneath, this option uses GCD to handle concurrent execution, most likely making use of dispatch groups (see Item 44). However, the implementation is not relevant here. Asking for reverse iteration is achieved through the NSEnumerationReverse option. Note that this is available only for situations in which it makes sense, such as arrays or ordered sets.

Overall, block enumeration has all the benefits of the other methods combined, and more. It is slightly more verbose than fast enumeration. But the benefits of having the index of enumeration, both the key and the value in dictionary enumeration, and the option to perform iterations concurrently are worth the extra source code.

Things to Remember

Image Enumerating collections can be achieved in four ways. The for loop is the most basic, followed by enumeration using NSEnumerator and fast enumeration. The most modern and advanced way is using the block-enumeration methods.

Image Block enumeration allows you to perform the enumeration concurrently, without any additional code, by making use of GCD. This cannot as easily be achieved with the other enumeration techniques.

Image Alter the block signature to indicate the precise types of objects if you know them.

0 0
原创粉丝点击