Objective-C 各种遍历方式对比

Mike Ash Friday Q&A 中文译文:Objective-C 各种遍历方式对比

作者 TommyWu
封面圖片: Objective-C 各种遍历方式对比

译文 · 原文: Friday Q&A 2010-04-09: Comparison of Objective-C Enumeration Techniques · 作者 Mike Ash

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


欢迎回来阅读又一期「星期五问答」。Preston Sumner 建议我谈谈在 Cocoa 中枚举(enumerating)集合的不同方式,以及如何实现「快速枚举」(Fast Enumeration)。这将是一个分为两部分的系列:本周我将探讨不同的枚举技术及其优缺点,然后下周我将带你在自定义对象上实现快速枚举。

设定基准线 为了建立比较基准,让我们考虑遍历一个存储 Objective-C 对象的 C 数组(C array)的情况:

id *array = ...;
NSUInteger length = ...;
for(NSUInteger i = 0; i < length; i++)
// do something with array[i]

对于单线程代码,这大约就是你能为对象枚举(object enumeration)达到的最快速度了。你只需承担循环带来的少量开销,这几乎是理论上最小的开销了。(如果你真想走极端,可以尝试手动展开循环(manually unrolling the loop),但这开始变得有些疯狂,而且实际上可能损害性能。)

当然,这种对象枚举方式在 Cocoa 应用中通常并不实用。我们很少遇到存储在 C 数组(C array)中的对象。更常见的情况是枚举一个实现了集合的对象。

NSEnumerator 在过去,Cocoa 中枚举集合的标准方式是使用 NSEnumerator

NSEnumerator *enumerator = [collection objectEnumerator];
id obj;
while((obj = [enumerator nextObject]))
// do something with obj

objectAtIndex: 对于那些偏爱更传统方式,或者不喜欢仅为枚举而创建全新对象的人,另一种枚举数组的方法就是反复调用其 objectAtIndex:(索引访问)方法:

NSUInteger length = [array count];
for(NSUInteger i = 0; i < length; i++)
{
id obj = [array objectAtIndex: i];
// do something with obj
}

除了冗长且容易出错之外,一个主要缺点是该方式根本无法用于枚举 NSSet 或 NSDictionary。反过来看,其显著优势在于 —— 若能谨慎管理循环索引 —— 在循环中修改数组是安全的,而其他任何枚举技术都无法做到这一点(除非你采用诸如枚举副本之类的方法)。

NSFastEnumeration 在 10.5 系统中,苹果终于解决了这个问题。他们通过引入 for/in 语法解决了冗长的问题,并将 for/in 构建在名为 NSFastEnumeration 的协议(protocol)之上,从而解决了速度问题。

for(id obj in collection)
// do something with obj

语法优美,性能出色,两者结合堪称完美。

基于 Blocks 的枚举 随着 Mac OS X 10.6(译注:Snow Leopard,2009 年发布)的推出,Apple 将 Blocks(代码块)引入了 Objective-C,同时也引入了基于 Blocks 的枚举。Blocks 非常适合用于创建像枚举这样的新控制结构,Apple 也因此为其集合类添加了基于 Blocks 的枚举方法:

[array enumerateObjectsUsingBlock: ^(id obj, NSUInteger index, BOOL *stop) {
// do something with obj
}];

首先,当你的需求超越简单枚举时。苹果提供了两种枚举选项:并发枚举和反向枚举。这两种方式都无法通过 for / in 语法直接实现。并发枚举很难通过其他方式实现,因此如果你的枚举能够利用多线程特性,这将极为实用。反向枚举虽然可以通过向数组发送 reverseObjectEnumerator 消息并将其作为 for / in 的目标对象来实现,但这仍然需要创建 NSEnumerator(枚举器)并间接通过它进行枚举,从而产生额外开销,因此基于块的方法可能更胜一筹。

其次,当你在枚举字典并需要同时获取键和对象时。for / in 语法每次只能提供一个对象,这意味着你必须先枚举键,再作为单独步骤向字典请求对象:

for(id key in dictionary)
{
id obj = [dictionary objectForKey: key];
// do something with key and obj
}

NSDictionary 提供了一个基于 block 的枚举方法,该方法会将 key(键)和 object(对象)直接传递给 block:

[dictionary enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) {
// do something with key and obj
}];

结论 对于任何代码,除非你确切知道存在性能问题,需要采用更复杂的方法来解决,否则永远优先选择最易维护和易读的技术。这一点在集合枚举(collection enumeration)中尤其明显,因为你在循环体内执行的操作所消耗的时间,几乎注定会远远超过循环机制本身所消耗的时间。

幸运的是,苹果公司使得我们在大多数情况下无需做任何权衡。在绝大多数场景中,for/in 语法同时是最优雅和最快速的集合枚举代码。对于极少数它并非最优选的情况,Mac OS X 10.6 提供了基于区块(blocks)的枚举构造来填补空白。除非必须支持 Mac OS X 10.4,否则你基本不应该再编写 NSEnumerator 循环。如果你需要在枚举过程中突变(mutate)数组,使用 objectAtIndex: 手动获取对象可能很方便,但除此之外,这种方法相比 for/in 并无优势。

本周到此为止。下周见,届时我将讲解如何构建自己的 NSFastEnumeration 协议实现。在此之前,请持续向我发送您想要讨论的主题创意。Friday Q & A(周五问答)栏目由读者投稿驱动,因此如果您有想在此探讨的话题,尽管发过来!下周的内容已经确定,但之后的安排完全开放。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-04-09-comparison-of-objective-c-enumeration-techniques.html

Welcome back to another edition of Friday Q&A. Preston Sumner has suggested that I talk about different ways of enumerating over collections in Cocoa, and how to implement Fast Enumeration. This will be a two part series: this week I will look at the different enumeration techniques and their pros and cons, and then next week I will take you through implementing Fast Enumeration on a custom object.

A Baseline To establishe a baseline for comparison, consider iterating over a C array of Objective-C objects:

id *array = ...;
NSUInteger length = ...;
for(NSUInteger i = 0; i < length; i++)
// do something with array[i]

For single-threaded code, this is about as fast as you’re going to get for object enumeration. You have a small amount of overhead from the loop, and it’s pretty much the minimum possible. (If you really want to get radical, you can try manually unrolling the loop, but that begins to get crazy, and could actually hurt performance.)

Of course, this sort of object enumeration is not usually practical in Cocoa apps. It’s rare that we encounter a C array of objects. Much more common is an object that implements a collection.

NSEnumerator In times past, the standard way of enumerating over a collection in Cocoa was to use NSEnumerator:

NSEnumerator *enumerator = [collection objectEnumerator];
id obj;
while((obj = [enumerator nextObject]))
// do something with obj

objectAtIndex: For those who preferred something more traditional, or who disliked creating a whole new object just for enumeration, another way to enumerate over an array was to simply call objectAtIndex: on it repeatedly:

NSUInteger length = [array count];
for(NSUInteger i = 0; i < length; i++)
{
id obj = [array objectAtIndex: i];
// do something with obj
}

A big disadvantage, besides being verbose and error-prone, is that it simply doesn’t work for enumerating NSSet or NSDictionary. Conversely, a big advantage is that, with careful management of the loop index, it’s safe to mutate the array inside the loop, something that’s not true of any other enumeration technique (unless you do something like enumerate over a copy instead).

NSFastEnumeration In 10.5, Apple finally solved this problem. They solved the verbosity problem by introducing the for/in syntax. And they solved the speed problem by building for/in on top of a protocol called NSFastEnumeration.

for(id obj in collection)
// do something with obj

Nice syntax, good performance, it’s a great combination.

Blocks-Based Enumeration With 10.6, Apple introduced blocks into Objective-C, and also introduced blocks-based enumeration. Blocks are a natural fit for creating new control constructs like enumeration, and Apple added blocks-based enumeration methods to their collections as well:

[array enumerateObjectsUsingBlock: ^(id obj, NSUInteger index, BOOL *stop) {
// do something with obj
}];

First is when you need something more than simple enumeration. Apple gives two enumeration options, to enumerate concurrently and to enumerate in reverse. Neither is directly supported by for/in syntax. Concurrent enumeration is very difficult to do any other way, so if your enumeration can take advantage of multithreading, this is extremely useful. Reverse enumeration can be done by sending reverseObjectEnumerator to an array and then using that as the target of a for/in, but this still has the overhead of creating an NSEnumerator and indirecting through it for enumeration, so the blocks-based method is probably a win.

Second is when you’re enumerating over a dictionary and need both keys and objects. The for/in syntax can only give you one object at a time. This means that you have to enumerate over keys, then ask the dictionary for objects as a separate step:

for(id key in dictionary)
{
id obj = [dictionary objectForKey: key];
// do something with key and obj
}

NSDictionary provides a blocks-based enumeration method that passes both key and object directly to the block:

[dictionary enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) {
// do something with key and obj
}];

Conclusion With any code, you should always prefer the technique which is easiest to maintain and read unless you know for sure that there’s a speed problem which would benefit from a more difficult approach. This is especially true with collection enumeration, where the work that you do inside the loop is virtually certain to dwarf the work that’s done by the loop itself.

Fortunately, Apple has made it so that we don’t have to make any tradeoffs in most cases. The for/in syntax is simultaneously the nicest and fastest code for enumerating over a collection in the majority of cases. For the rare cases where it’s not the best, 10.6 provides blocks-based enumeration constructs which fill in the holes. You should pretty much never write an NSEnumerator loop unless you have to support 10.4. Manually fetching objects using objectAtIndex: can be handy if you need to mutate the array while enumerating, but besides that has no advantage over for/in.

That’s it for this week. Come back next week, when I’ll talk about how to build your own implementation of the NSFastEnumeration protocol. Until then, keep sending me your ideas for topics to talk about. Friday Q&A is driven by reader submissions, so if you have a topic you’d like to see discussed here, send it in! Next week is already booked, but after that, the future is wide open.