实现快速枚举 Fast Enumeration

Mike Ash Friday Q&A 中文译文:实现快速枚举 Fast Enumeration

作者 TommyWu
封面圖片: 实现快速枚举 Fast Enumeration

译文 · 原文: Friday Q&A 2010-04-16: Implementing Fast Enumeration · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2010-04-16-implementing-fast-enumeration.html 发布:2010-04-16 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


上周我讨论了 Objective-C 中可用于枚举集合的各种选项。本周我将以如何在你的程序中实现 Fast Enumeration(快速枚举)的指南来完成关于枚举的讨论。

基础
实现 Fast Enumeration 有两个好处。一是你随后可以在 for / in 循环中将你的对象作为目标使用。二是通过良好的实现,这种枚举将非常高效。

实现 Fast Enumeration 是通过实现 NSFastEnumeration 协议来完成的。该协议只包含一个方法:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len;
typedef struct {
unsigned long state;
id *itemsPtr;
unsigned long *mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;

解析字段与参数
要理解如何实现这个方法,你需要明白所有字段、参数以及返回值的含义。我会打乱顺序来讲解。

此方法的目标是返回一系列对象数组。每次调用返回一个数组,允许批量返回对象。为了速度,这里使用 C 语言数组,因此需要一个指针和一个长度。

长度由方法的返回值提供 —— 这也是方法名中count的含义。实际数组存放在NSFastEnumerationState结构体的itemsPtr字段中。这两个值共同定义了方法返回的数组。

NSFastEnumeration的设计允许返回指向内部存储的指针。然而并非所有数据结构都适合这样做,因此它也允许将对象复制到调用方提供的数组中。这个由调用方提供的数组是stackbuf,其大小由len参数指定。

NSFastEnumeration 还被设计用于在枚举期间检测集合是否被修改,若发生这种情况则会抛出异常。mutationsPtr 应指向一个值,当集合被修改时该值会发生变化。

这就是全部内容了。NSFastEnumerationState 中唯一还未介绍的字段是 stateextra。这些是自由格式字段,被调用方可以使用它们存储任何它认为有用的值。

生成的循环代码 现在我们知道这些字段的用途了,但要真正理解其工作原理,最好了解编译器会生成什么样的代码。你这样写:

for(id obj in collection)
{
// body
}

编译器会在栈上分配一个 NSFastEnumerationState(快速枚举状态)结构体以及一个栈缓冲区。它创建了两个嵌套循环:外层循环反复调用 countByEnumeratingWithState:... 方法,内层循环则遍历该方法返回的数组。最终生成的代码大致如下:

// declare all the local state needed
NSFastEnumerationState state = { 0 };
id stackbuf[16];
BOOL firstLoop = YES;
long mutationsPtrValue;
// outer loop
NSUInteger count;
while((count = [collection countByEnumeratingWithState: &state objects: stackbuf count: 16]))
{
// check for mutation, but only after the first loop
// (note that I'm not sure whether the real compiler puts this
// in the inner loop or outer loop, and it could conceivably
// change from one compiler version to the next)
if(!firstLoop && mutationsPtrValue != *state.mutationsPtr)
@throw ..mutation exception...
firstLoop = NO;
mutationsPtrValue = *state.mutationsPtr;
// inner loop over the array returned by the NSFastEnumeration call
id obj;
for(NSUInteger index = 0; index < count; index++)
{
obj = state.itemsPtr[index];
// body
}
}

一次返回一个对象 NSFastEnumeration 的一个重要目标是通过批量枚举来实现速度提升。一次返回一个对象违背了这一初衷。然而,这种方法易于实现,且仍能让你享有使用 for / in 语法的好处。本着避免过早优化的精神,如果一次返回一个对象很容易实现,那就去做吧。

例如,假设你有一个链表类:

@implementation LinkedList : NSObject
{
struct Node *listHead;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// plan of action: extra[0] will contain pointer to node
// that contains next object to iterate
// because extra[0] is a long, this involves ugly casting
if(state->state == 0)
{
// state 0 means it's the first call, so get things set up
// we won't try to detect mutations, so make mutationsPtr
// point somewhere that's guaranteed not to change
state->mutationsPtr = (unsigned long *)self;
// set up extra[0] to point to the head to start in the right place
state->extra[0] = (long)listHead;
// and update state to indicate that enumeration has started
state->state = 1;
}
// pull the node out of extra[0]
struct Node *currentNode = (struct Node *)state->extra[0];
// if it's NULL then we're done enumerating, return 0 to end
if(!currentNode)
return NULL
// otherwise, point itemsPtr at the node's value
state->itemsPtr = &currentNode->value
// update extra[0]
if(currentNode)
state->extra[0] = (long)currentNode->next;
// we're returning exactly one item
return 1;
}

批量返回复制对象 假设上述方法确实太慢,你想让它更快。可以通过批量返回对象来实现。由于链表(linked list)中的对象不是连续存储的,因此需要将对象复制到 stackbuf 中来实现此目的。虽然不能保证 stackbuf 的大小,但我们可以假定它足够大,足以支持这种操作。代码看起来会像这样:(译注:现代系统中,stackbuf 的大小及可用性可能因环境而异)

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// plan of action: pretty much the same as before,
// with extra[0] pointing to the next node to use
// we just iterate over multiple nodes at once
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->extra[0] = (long)listHead;
state->state = 1;
}
// pull the node out of extra[0]
struct Node *currentNode = (struct Node *)state->extra[0];
// keep track of how many objects we iterated over so we can return
// that value
NSUInteger objCount = 0;
// we'll be putting objects in stackbuf, so point itemsPtr to it
state->itemsPtr = stackbuf;
// loop through until either we fill up stackbuf or run out of nodes
while(currentNode && objCount < len)
{
// fill current stackbuf location and move to the next
*stackbuf++ = currentNode->value
// move to next node
currentNode = currentNode->next;
// and keep our count
objCount++;
}
// update extra[0]
if(currentNode)
state->extra[0] = (long)currentNode->next;
return objCount;
}

返回批量内部指针 为获得最佳效率,你可以返回一个指向连续存储对象的指针。例如,假设你有一个像这样的简单数组类:

@interface Array : NSObject
{
id *pointer;
NSUInteger count;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->itemsPtr = pointer;
state->state = 1;
return count;
}
else
return 0;
}

关于临时对象的说明

这项技术在谨慎使用时,也能用于更复杂的数据结构。若你有一系列连续对象指针,你可以依次返回每个指针,从而实现对所有对象的高效枚举(efficient enumeration)。你可以充分利用 ** 额外值(extra values)** 来追踪你在内部数据结构中的位置。

在额外值中存储 Objective-C 对象可能会很有用:

state->extra[1] = (long)[NSArray arrayWith...];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
for(id obj in collection)
{
// do stuff with obj
[pool release];
pool = [NSAutoreleasePool new];
}
[pool release];

确实没有通用的解决方案。(我构思过一个完全疯狂的方案,通过跟踪栈指针位置来判断何时可以安全销毁临时对象,但这方案确实…… 极其疯狂。)如果可能的话,尽量避免像这样在extra字段中存储临时的 Objective-C 对象。如果必须这么做,请记住在使用此对象的 for / in 循环中需要谨慎处理自动释放池(autorelease pool)。鉴于你很可能是自己实现的 NSFastEnumeration 的唯一使用者,这个约束条件是合理的,但你必须意识到这一点。

结论 实现 NSFastEnumeration 能让你用简洁优雅的语法来枚举自定义对象 —— 这些对象在概念上是其他对象的集合。作为额外收获,它通常还能提升枚举速度。虽然 NSFastEnumeration 初看可能令人生畏,但根据你追求的优化程度和内部数据结构的复杂度,编写它的实现其实相当简单。

本周的周五问答到此结束。请在七天后回来继续收获更多精彩内容。周五问答由用户投稿驱动,所以在此期间,如果您有任何希望在此讨论的话题创意,请发送给我们!


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-04-16-implementing-fast-enumeration.html

Last week I discussed the various options available in Objective-C for enumerating over a collection This week I’m going to finish up the discussion of enumeration with a guide on how to implement Fast Enumeration in your own program.

Basics There are two benefits to implementing Fast Enumeration. One is that you can then use your object as the target in a for/in loop. The other is that, with a good implementation, such enumeration will be fast.

Implementing Fast Enumeration is accomplished by implementing the NSFastEnumeration protocol. This protocol contains only a single method:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len;
typedef struct {
unsigned long state;
id *itemsPtr;
unsigned long *mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;

Deciphering Fields and Parameters To understand how to implement this method, you need to understand what all of the fields and parameters mean, as well as the return value. I’ll take these out of order.

The objective of this method is to return a series of arrays of objects. Each call returns one array, which allows objects to be returned in bulk. For speed, this uses a C array, which means that it needs a pointer and a length.

The length is provided by the return value of the method. That’s what the count refers to in the name of the method. The array is actually the itemsPtr field of the NSFastEnumerationState struct. These two values together define the array returned by the method.

NSFastEnumeration is designed to allow returning a pointer to internal storage. However, not all data structures fit well with that, so it’s also designed to allow copying objects into an array provided by the caller. That caller-provided array is stackbuf, and its size is given by len.

NSFastEnumeration is also designed to detect when a collection is mutated while being enumerated, and throw an exception if this occurs. mutationsPtr is indended to be pointed to a value which changes if the collection is mutated.

That’s just about everything. The only fields I haven’t covered yet are the state and extra fields of NSFastEnumerationState. These are just freeform fields which the callee can use to store whatever values it finds useful.

Generated Loop Code Now we know what all these things are for, but to really understand how this stuff works, it’s best to understand what kind of code the compiler generates. You write this:

for(id obj in collection)
{
// body
}

The compiler creates an NSFastEnumerationState on the stack, as well as a stack buffer. It creates two nested loops, one which repeatedly calls countByEnumeratingWithState:… and one which loops over the array it returns. It ends up being something like this:

// declare all the local state needed
NSFastEnumerationState state = { 0 };
id stackbuf[16];
BOOL firstLoop = YES;
long mutationsPtrValue;
// outer loop
NSUInteger count;
while((count = [collection countByEnumeratingWithState: &state objects: stackbuf count: 16]))
{
// check for mutation, but only after the first loop
// (note that I'm not sure whether the real compiler puts this
// in the inner loop or outer loop, and it could conceivably
// change from one compiler version to the next)
if(!firstLoop && mutationsPtrValue != *state.mutationsPtr)
@throw ..mutation exception...
firstLoop = NO;
mutationsPtrValue = *state.mutationsPtr;
// inner loop over the array returned by the NSFastEnumeration call
id obj;
for(NSUInteger index = 0; index < count; index++)
{
obj = state.itemsPtr[index];
// body
}
}

Returning One Object At a Time A major point of NSFastEnumeration is to achieve speed through bulk enumeration. Returning one object at a time defeats that point. However, it’s easy to implement, and still gets you the benefit of being able to use for/in syntax. In the spirit of avoiding premature optimization, if returning one object at a time is easy, then go for it.

As an example, imagine you have a linked list class:

@implementation LinkedList : NSObject
{
struct Node *listHead;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// plan of action: extra[0] will contain pointer to node
// that contains next object to iterate
// because extra[0] is a long, this involves ugly casting
if(state->state == 0)
{
// state 0 means it's the first call, so get things set up
// we won't try to detect mutations, so make mutationsPtr
// point somewhere that's guaranteed not to change
state->mutationsPtr = (unsigned long *)self;
// set up extra[0] to point to the head to start in the right place
state->extra[0] = (long)listHead;
// and update state to indicate that enumeration has started
state->state = 1;
}
// pull the node out of extra[0]
struct Node *currentNode = (struct Node *)state->extra[0];
// if it's NULL then we're done enumerating, return 0 to end
if(!currentNode)
return NULL
// otherwise, point itemsPtr at the node's value
state->itemsPtr = &currentNode->value
// update extra[0]
if(currentNode)
state->extra[0] = (long)currentNode->next;
// we're returning exactly one item
return 1;
}

Returning Copied Objects in Bulk Let’s say that it turns out the above method really is too slow and you want to make it faster. You can do this by returning objects in bulk. Because the objects in the linked list aren’t stored contiguously, you have to do this by copying objects into the stackbuf. While no guarantee is given as to the size of stackbuf, we can assume that it’s made large enough to justify this sort of thing. Here’s how the code would look:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// plan of action: pretty much the same as before,
// with extra[0] pointing to the next node to use
// we just iterate over multiple nodes at once
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->extra[0] = (long)listHead;
state->state = 1;
}
// pull the node out of extra[0]
struct Node *currentNode = (struct Node *)state->extra[0];
// keep track of how many objects we iterated over so we can return
// that value
NSUInteger objCount = 0;
// we'll be putting objects in stackbuf, so point itemsPtr to it
state->itemsPtr = stackbuf;
// loop through until either we fill up stackbuf or run out of nodes
while(currentNode && objCount < len)
{
// fill current stackbuf location and move to the next
*stackbuf++ = currentNode->value
// move to next node
currentNode = currentNode->next;
// and keep our count
objCount++;
}
// update extra[0]
if(currentNode)
state->extra[0] = (long)currentNode->next;
return objCount;
}

Returning a Bulk Interior Pointer For best efficiency, you can return a pointer to contiguously stored objects. For example, say you have a simple array class like this:

@interface Array : NSObject
{
id *pointer;
NSUInteger count;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->itemsPtr = pointer;
state->state = 1;
return count;
}
else
return 0;
}

This technique can also be used, with some care, for more complex data structures. If you have a series of contiguous object pointers, you can return pointers to each one in turn, which will result in efficient enumeration over all of the objects in sequence. You can make good use of the extra values to keep track of where you are in your internal data structure.

A Note on Temporary Objects You may find it useful to store Objective-C objects in the extra values:

state->extra[1] = (long)[NSArray arrayWith...];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
for(id obj in collection)
{
// do stuff with obj
[pool release];
pool = [NSAutoreleasePool new];
}
[pool release];

There’s really no general way to solve this. (I’ve concocted a completely insane scheme which involves tracking the position of the stack pointer to know when it’s safe to destroy temporary objects, but it’s, well, completely insane.) If you can, try to avoid storing temporary Objective-C objects in extra like this. And if you must do it, just keep in mind that you have to be careful with autorelease pools in the for/in loops that you use with this object. Since you’re likely to be the only client of your NSFastEnumeration implementation, this is a reasonable constraint to make, but it’s something that you have to be aware of.

Conclusion Implementing NSFastEnumeration allows you to use nice, simple syntax for enumerating over your custom objects which are conceptually collections of other objects. As a bonus, it will usually speed up that enumeration as well. And while NSFastEnumeration can look daunting at first glance, it’s actually pretty easy to write an implementation of it, depending on just how hard-core you want to get and how complex your internal data structures are.

That’s it for this week’s Friday Q&A. Come back in another seven days for more gooey goodness. Friday Q&A is driven by user submissions, so in the meantime, if you have an idea for a topic that you’d like to see covered here, please send it in!