避免"NSArray在枚举时被修改"错误

46

我有一个NSMutableArray,它存储了Box2d物理仿真的鼠标关节。当使用多个手指玩时,我会收到异常,指出

NSArray在枚举时被更改了

我知道这是因为我正在从数组中删除对象并同时枚举它,从而使enum无效。

我想知道的是,解决这个问题的最佳策略是什么?我在网上看到了一些解决方案:@synchronized、枚举之前复制数组或将触摸关节放入垃圾数组进行以后的删除(我不确定这是否有效,因为我需要在从世界中删除关节后立即从数组中删除鼠标关节)。

11个回答

49
您可以在不使用枚举器的情况下进行迭代。这意味着使用常规的for循环,在删除对象时:将索引变量减一并使用continue;继续循环。如果您在进入for循环之前缓存了数组的计数,则在删除对象时确保也要减少计数。
无论如何,我不认为带有待删除对象的数组会有问题。我不知道您所面临的具体情况和涉及的技术,但从理论上讲不应该有问题。因为在大多数情况下,当使用此方法时,您可以在第一次遍历中什么都不做,并在遍历待删除数组时执行实际操作。而如果在第一次枚举中,您需要再次检查相同的数组以查看对象是否不存在,那么只需添加一个检查,以查看它们是否在待删除数组中即可。
总之,希望能帮到您。祝好运!

2
无论如何,希望我有所帮助;肯定帮了我很多!谢谢! - Jarrod
这个方法对于使用NSMutableSet而不是数组的同样问题无效。你有什么建议吗? - Victor Engel
1
@VictorEngel 是的,请创建一个包含您想要删除的所有项的单独集合,然后在原始集合(或其mutableCopy)上使用"minusSet",并以第二个集合作为参数。 :) - daniel.gindi
在iOS6中使用块枚举方法时,我遇到了这个错误。 - Allen

38

你可以这样做:

NSArray *tempArray = [yourArray copy];
for(id obj in tempArray) {
    //It's safe to remove objects from yourArray here.
}
[tempArray release];

27
这不是真的。即使我进行了枚举,我仍会遇到一个突变异常。 - Brendt
1
tempArray 应该像这样初始化 NSArray *tempArray = [NSArray arrayWithArray:yourArray] 吗?看起来这段代码不会达到 OP 的预期。 - Tim Arnold
2
@TimArnold 我非常确定那会得到相同的结果。 - edc1591
@Brendt,请展示你的代码片段。这个答案对我来说似乎是正确的。因为我们使用不同的容器,它会保留yourArray中的所有对象。 - Evgen Bodunov
1
使用 for (int = 0; i < yourArray.count; i++) { /* 在此处删除yourArray中的对象是安全的。*/ },这不会使用数组的枚举器。 - Itachi

38

最简单的方法是反向枚举数组,这意味着当您删除一个对象时,下一个索引不会受到影响。

for (NSObject *object in [myMutableArray reverseObjectEnumerator]) {
    // it is safe to test and remove the current object      
    if (AddTestHere) {
        [myMutableArray removeObject: object];
    }
}

最简单且高效的答案。 - IrelDev
这不是正确的。根据reverseObjectEnumerator文档:“当您在可变的NSArray子类中使用此方法时,在枚举期间不能修改数组。” - saagarjha
这是一个一般性的警告,但显然比那更微妙。通过在当前索引之前插入或删除NSMutableArray中的元素将会创建问题,但这不是正在此处进行的操作。相信我,我已经使用这种技术6年以上,没有任何问题。它完全可以正常工作。 - martinjbaker
1
仅仅因为它“运行良好”并不意味着它是正确的。你甚至可以调用私有API,它也会“工作良好”,直到苹果决定更改它,那么你将会崩溃。你正在依赖于内部实现细节,并明确地做了文档告诉你不要做的事情。请使用其中一种支持的方式来完成此操作。 - saagarjha
别傻了,我依赖于哪个“内部实现细节”? - martinjbaker

6

使用锁定操作(@synchronized)比一遍又一遍地复制整个数组要快得多。当然,这取决于数组有多少元素以及执行频率如何。 假设您有10个线程同时执行此方法:

- (void)Callback
{
  [m_mutableArray addObject:[NSNumber numberWithInt:3]];
  //m_mutableArray is instance of NSMutableArray declared somewhere else

  NSArray* tmpArray = [m_mutableArray copy];
  NSInteger sum = 0;
  for (NSNumber* num in tmpArray)
      sum += [num intValue];

  //Do whatever with sum
}

每次都会复制n+1个对象。你可以在此处使用锁定,但如果要迭代100k个元素呢?数组将被锁定直到迭代完成,其他线程将不得不等待锁定释放。我认为在这里复制对象更有效,但这也取决于对象的大小以及您在迭代中所做的工作。锁定时间应始终保持最短。因此,我会在类似这样的情况下使用锁。

- (void)Callback
{
  NSInteger sum = 0;
  @synchronized(self)
  {
    if(m_mutableArray.count == 5)
      [m_mutableArray removeObjectAtIndex:4];
    [m_mutableArray insertObject:[NSNumber numberWithInt:3] atIndex:0];

    for (NSNumber* num in tmpArray)
      sum += [num intValue];
  }
  //Do whatever with sum
}

2
for(MyObject *obj in objQueue)
{
//[objQueue removeObject:someof];//NEVER removeObject in a for-in loop
    [self dosomeopearitions];
}

//instead of remove outside of the loop
[objQueue removeAllObjects];

1

使用for循环而不是枚举是可以的。但是一旦你开始删除数组元素,如果你正在使用线程,请小心。仅仅递减计数器是不够的,因为你可能会删除错误的内容。其中一种正确的方法是创建数组的副本,迭代副本并从原始数组中删除。

edc1591是正确的方法。

[Brendt:需要在迭代副本时从原始数组中删除]


感谢你提供的提示,让我明白了在迭代副本时从原始数据中删除元素的方法。 - 最白目

1

以上的方法对我都无效,但是这个有效:

 while ([myArray count] > 0) 
 {
      [<your delete method call>:[myArray objectAtIndex:0]];
 }

注意:这将全部删除。如果您需要选择要删除的项目,则此方法将无效。

1
清空一个数组并不需要逐个删除对象。你只需要这么写:myArray = [NSMutableArray init];修改不仅仅是删除,它也包括修改对象,这通常是你想要的。无论哪种情况,“枚举和修改”数组的正确方法是先创建数组的副本,然后使用该副本来枚举对象,同时将原始数组按预期进行修改。 - Kaveh Vejdani

1

我曾经遇到过同样的问题,解决方法是对复制品进行枚举并改变原始数据,就像edc1591所正确提到的那样。我尝试了这段代码,不再出现错误。如果你仍然看到错误,那意味着你在for循环中仍然更改的是复制品而不是原始数据。

NSArray *copyArray = [[NSArray alloc] initWithArray:originalArray];

for (id obj in copyArray) { 
    // tweak obj as you will 
    [originalArray setObject:obj forKey:@"kWhateverKey"];
}

2
setObject:forKey 是字典的方法,而不是数组(此外,它必须是可变版本)。否则,该方法是正确的。 - Nicolas Miari

0
也许你在枚举mutableArray时,删除或添加了其中的对象。 在我的情况下就出现了这个错误。

0

我遇到了这个错误,原因是多线程的情况。一个线程在枚举一个数组,而另一个线程同时从同一个数组中移除对象。希望这能帮助到某些人。


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