【问题标题】:Is this an inefficient way of using fast enumeration?这是使用快速枚举的低效方式吗?
【发布时间】:2012-09-05 12:29:30
【问题描述】:

我不完全了解枚举的工作原理的细节,但比较以下两种情况:

for(NSObject *object in self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array) {
    // do something
}

对比

NSArray *array = self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array;
for(NSObject *object in array) {
     // do something
 }

在第一个示例中,它是否会在每次迭代时遍历整个链来获取数组?我应该使用第二种方式吗?

【问题讨论】:

    标签: objective-c ios fast-enumeration


    【解决方案1】:

    当 Apple 推出快速枚举时,我在 WWDC,并且(我记得)当时我们被告知右手对象被移动到临时对象中。另外,一定是因为这行得通:

    for(id foo in [myCollection reverseObjectEnumerator])

    您可以看到执行快速枚举的集合采用“快速枚举协议”(NSFastEnumeration),它有一种方法:

    – countByEnumeratingWithState:objects:count:
    

    该方法返回一个 C 对象数组,可以让枚举非常快速地进行,再次支持右侧的一次性使用。


    现在,说了这么多,目前 Apple 建议开发人员(在 WWDC)使用块枚举,他们声称这样更快并且生成的代码更少:

    [myCollection enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
    {
    ... your code
    } ];
    

    我喜欢做的不是使用“id obj”,而是实际类型(以避免在块中强制转换):

    [myCollection enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop)
    {
    ... your code
    } ];
    

    当我这样做时,编译器和分析器 (4.4) 都不会抱怨。

    如果你需要在这个方法之外设置一个变量,那么你必须让它成为一个块变量:

    __block int foo = 0;
    [myCollection enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop)
    {
       foo = MAX(foo, [num integerValue]);
    } ];
    

    编辑:作为澄清,您的问题的直接答案是“否”,语句“self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array”被评估一次,最终对象存储为临时堆栈。此外,您可以对块枚举使用相同的技术 - 语句被评估一次,最终返回的对象用于枚举。

    __block int foo = 0;
    [self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop)
    {
       foo = MAX(foo, [num integerValue]);
    } ];
    

    EDIT2:我在 SO 上找到了另一个讨论相同主题的线程。关于块枚举,我错过的一点是,您可以使用稍微复杂的方法指定它们应该同时(或反向)运行:

    enumerateObjectsWithOptions:usingBlock:
    

    随着 iOS 设备获得越来越多的核心,这可能是一个巨大的胜利,具体取决于您正在做什么。

    @bbum 对问题的回答(以及其他问题)是here。

    【讨论】:

    • 该方法返回一个 C 对象数组,可以让枚举非常快速地进行,再次支持右侧的一次性使用。 +1 -- 对。如果意图是“快速”,那将是正确的方法。集合是枚举的,而不是任意的表达式。如果在此构造中需要速度或正确性,则表达式返回的变体甚至都不是考虑因素。
    • 大卫,我不记得曾经发表过那个声明。哪个 WWDC 演示文稿这么说的?另外,我只是进行了一个快速实验,对于几种不同的情况,我永远看不到调用 enumerateObjectsUsingBlock 比快速枚举数组更快。在调试构建中,快速枚举要快两个数量级。调试虽然没有任何意义。在发布版本中,它们非常接近,但块枚举从未如此快。
    • @JodyHagins 等等,调试版本和发布版本有什么区别?我认为这只是证书和配置的问题吗?
    • 您可以通过更改方案中的设置自行确定差异,但通常调试版本没有优化,而发布版本可以实现大多数优化。您可以通过选择 Scheme 轻松切换,然后选择 Run,然后在 debug/release 之间切换。
    • @JodyHagins 你真的看过所有 WWDC 视频吗? ;)
    【解决方案2】:

    这可能是特定于编译器的(即未定义)。如果您对此感到困扰,请添加一些计时代码并找出自己:

    #import <sys/time.h>
    
    static unsigned getTickCount()
    {
        struct timeval tv;
        gettimeofday(&tv, 0);
        return (unsigned)((tv.tv_sec * 1000) + (tv.tv_usec / 1000));
    }
    

    ...

    unsigned startTime = getTickCount();
    for(NSObject *object in self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array) {
        // do something
    }
    unsigned endTime = getTickCount();
    NSLog(@"That took %umS", endTime - startTime);
    

    但是,您必须拥有一个相当大的数组才能注册0 以上的任何内容。

    【讨论】:

    • 在myParent getter 中设置断点并检查它是否在整个循环中只被命中一次会更容易(也更可靠)...
    • @omz 是的,好点,但这不会告诉你它是否重要。时间会的。
    • 最后一点也是为什么每次迭代都访问getter链无关紧要的原因。在第一次访问之后,方法的缓存机制就会启动,如果您没有非常庞大的数据集,那么使用什么都没关系。
    • @JustSid 你怎么知道这些 getter 到底做了什么?也许他们需要执行某种昂贵的计算才能获得结果。那么他们被调用 1000 次还是只调用一次就很重要了。 (此外,方法调用永远不会“免费”)
    • @omz 非常正确的观点,对不起,我的措辞有点愚蠢。我的意思是这很可能无关紧要,而且这个问题听起来很像过早的优化。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-10
    • 1970-01-01
    • 1970-01-01
    • 2020-05-19
    • 2013-04-26
    • 1970-01-01
    相关资源
    最近更新 更多