何时使用enumerateObjectsUsingBlock和for循环?

151
除了明显的区别:
  • 当您需要索引和对象时,请使用enumerateObjectsUsingBlock
  • 当您需要修改局部变量时,请勿使用enumerateObjectsUsingBlock(我对此错误,参见bbum的答案)

for (id obj in myArray)也可以正常工作时,enumerateObjectsUsingBlock通常被认为更好还是更差?有什么优缺点(例如,它的性能更好还是更差)?


1
如果我需要当前的索引,我喜欢使用它。 - Besi
参见:https://dev59.com/smoy5IYBdhLWcg3wfeMR - Simon Whitaker
6个回答

357

最终,在上下文中使用您更想要使用且更自然的模式。

虽然for(... in ...)非常方便且语法简洁,但enumerateObjectsUsingBlock:具有许多可能或可能不会证明有趣的功能:

  • enumerateObjectsUsingBlock:将与快速枚举一样快或更快(for (...in ...)使用NSFastEnumeration支持实现枚举)。 快速枚举需要从内部表示转换为用于快速枚举的表示。 这其中存在开销。 基于块的枚举允许集合类像本地存储格式的最快遍历一样快速枚举内容。 对于数组来说可能不相关,但对于字典来说可能是很大的差别。

  • "当您需要修改局部变量时,请勿使用enumerateObjectsUsingBlock" - 不正确; 您可以将本地变量声明为__block,并在块中可写。

  • enumerateObjectsWithOptions:usingBlock:支持并发或逆向枚举。

  • 对于字典,基于块的枚举是检索键和值同时的唯一方法。

个人而言,我更经常使用enumerateObjectsUsingBlock:而不是for(... in ...),但 - 再次 - 是个人选择。


17
哇,非常有启发性。我希望我能接受这两个答案,但我会选Chuck的,因为它更符合我的共鸣。同时,在搜索__block时,我发现了你的博客(http://www.friday.com/bbum/2009/08/29/blocks-tips-tricks/)并学到了更多知识。谢谢。 - Paul Wheeler
9
记录一下,基于块的枚举并不总是比其他方式“更快或更快”。http://www.mikeabdullah.net/slow-block-based-dictionary-enumeration.html - Mike Abdullah
2
@VanDuTran 只有在你告诉它们在单独的线程上执行时,块才会在单独的线程上执行。除非你使用枚举的并发选项,否则它将在与调用相同的线程上执行。 - bbum
2
Nick Lockwood在这方面写了一篇非常好的文章,看起来enumerateObjectsUsingBlock对于数组和集合仍然比快速枚举慢得多。我想知道为什么?http://iosdevelopertips.com/objective-c/high-performance-collection-looping-objective-c.html - Bob Spryn
2
虽然这是一种实现细节,但在这个答案中应该提到两种方法之间的差异之一是enumerateObjectsUsingBlock将每次块调用都包装在自动释放池中(至少在OS X 10.10中是这样)。这解释了与不这样做的for in相比的性能差异。 - Pol
显示剩余8条评论

83

对于简单的枚举,仅使用快速枚举(即for...in...循环)是更加惯用的选项。块方法可能会稍微快一点,但在大多数情况下这并不重要——很少有程序是CPU限制的,即使是那些受限于计算本身而非循环的情况也很少成为瓶颈。

一个简单的循环也更易读。以下是两个版本的样板代码:

for (id x in y){
}

[y enumerateObjectsUsingBlock:^(id x, NSUInteger index, BOOL *stop){}];

即使你添加一个变量来跟踪索引,简单的循环更易于阅读。

那么什么时候应该使用enumerateObjectsUsingBlock:呢?当你需要存储一个块以便稍后执行或在多个地方执行时。当你实际上将块用作一等函数而不是过度复杂的循环体替代品时,它非常有用。


5
在所有情况下,enumerateObjectsUsingBlock:的速度要么与快速枚举相同,要么更快。for(... in ...) 使用快速枚举,这需要集合提供一些内部数据结构的中间表示。正如您所指出的那样,这可能是无关紧要的。 - bbum
5
当你需要把一个代码块存储到稍后执行或多个地方使用时,使用闭包会很好。这在你需要将代码块作为一等函数来使用而不是作为循环主体的繁琐替换时非常有用。 - Steve
7
我的测试结果显示,enumerateObjects...比使用循环的快速枚举实际上可能更慢。我运行了这个测试数千次;块和循环的主体都是相同的一行代码:[(NSOperation *)obj cancel];。平均值为:使用快速枚举的循环 - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009,而对于块--[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043。奇怪的是时间差异如此之大和一致,但显然这是一个非常具体的测试案例。 - chown

44
尽管这个问题很旧,但事情没有改变,已接受的答案是不正确的。 enumerateObjectsUsingBlock API的作用并不是取代for-in,而是为了完全不同的用例:
  • 它允许应用任意的、非本地的逻辑。即你不需要知道块执行什么操作就可以在数组上使用它。
  • 对于大集合或重计算的并发枚举(使用withOptions:参数)

使用for-in进行快速枚举仍然是枚举集合的惯用法

快速枚举具有简洁的代码、可读性和额外的优化,使其异常快速。比一个老的C for-loop更快!

一个快速测试结论是,在iOS 7上的2014年,enumerateObjectsUsingBlock始终比for-in慢700%,基于一个100项数组的1mm次迭代)。

性能在这里是一个真正实际的关注点吗?

绝对不是,除了很少的例外。

重点是要证明,没有很好的理由使用enumerateObjectsUsingBlock:而不是for-in。它不能使代码更易读……或更快……或线程安全(另一个常见误解)。

选择取决于个人偏好。对我来说,惯用和可读性选项胜出。在这种情况下,使用for-in进行快速枚举。

基准测试:

NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 100; i++) {
    arr[i] = [NSString stringWithFormat:@"%d", i];
}
int i;
__block NSUInteger length;

i = 1000 * 1000;
uint64_t a1 = mach_absolute_time();
while (--i > 0) {
    for (NSString *s in arr) {
        length = s.length;
    }
}
NSLog(@"For-in %llu", mach_absolute_time()-a1);

i = 1000 * 1000;
uint64_t b1 = mach_absolute_time();
while (--i > 0) {
    [arr enumerateObjectsUsingBlock:^(NSString *s, NSUInteger idx, BOOL *stop) {
        length = s.length;
    }];
}
NSLog(@"Enum %llu", mach_absolute_time()-b1);

结果:

2014-06-11 14:37:47.717 Test[57483:60b] For-in 1087754062
2014-06-11 14:37:55.492 Test[57483:60b] Enum   7775447746

4
我可以确认,在 MacBook Pro Retina 2014 上运行相同的测试,使用 enumerateObjectsUsingBlock 实际上要慢 5 倍。这显然是由于自动释放池包装了块的每次调用,而在 for in 的情况下不会发生这种情况。 - Pol
2
我确认在真实的iPhone 6 iOS9上,使用Xcode 7.x构建,enumerateObjectsUsingBlock:仍然比较慢,慢了4倍。 - Cœur
1
谢谢确认!我希望这个答案不会被埋没...有些人喜欢enumerate语法,因为它感觉像FP,而且不想听性能分析。 - Adam Kaplan
1
顺便说一下,这不仅仅是由于自动释放池的原因。使用 enumerate: 时还有更多的堆栈推送和弹出。 - Adam Kaplan

24
为了回答关于性能的问题,我使用了性能测试项目进行了一些测试。我想知道向数组中的所有对象发送消息的三个选项中哪个是最快的。
这些选项是: 1)makeObjectsPerformSelector
[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) 快速枚举和正常消息发送

for (id item in arr)
{
    [item _stubMethod];
}

3) enumerateObjectsUsingBlock和常规消息发送

[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
 {
     [obj _stubMethod];
 }];

结果表明,makeObjectsPerformSelector是迄今为止最慢的。它比快速枚举慢了两倍。而enumerateObjectsUsingBlock是最快的,比快速迭代快15-20%左右。

因此,如果你非常关注最佳性能,请使用enumerateObjectsUsingBlock。但要记住,在某些情况下,枚举集合所需的时间被运行每个对象要执行的任何代码所耗费的时间所淹没。


你能指出你具体的测试吗?你似乎给出了错误的答案。 - Cœur

3

如果你想要打破嵌套循环,使用enumerateObjectsUsingBlock作为外循环是非常有用的。

例如:

[array1 enumerateObjectsUsingBlock:^(id obj1, NSUInteger idx, BOOL * _Nonnull stop) {
  for(id obj2 in array2) {
    for(id obj3 in array3) {
      if(condition) {
        // break ALL the loops!
        *stop = YES;
        return;
      }
    }
  }
}];

使用goto语句是一种可选方案。

1
你可以像在这里所做的那样,直接从该方法中返回 :-D - Adam Kaplan

1
感谢@bbum和@Chuck对性能进行全面比较。很高兴知道这是微不足道的。我似乎选择了以下内容:
  • for (... in ...) - 作为我的默认goto。对我来说更直观,具有更多的编程历史而不是真正的偏好 - 跨语言重用,由于IDE自动完成大多数数据结构而减少打字:P。

  • enumerateObject... - 当需要访问对象和索引时。以及当访问非数组或字典结构时(个人偏好)

  • for (int i=idx; i<count; i++) - 对于数组,当我需要从非零索引开始时


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接