使用forin遍历同时修改数组产生的问题
-
今天在看各类面试题的时候遇到了一个诡异的问题, 以前都没有考虑过, 代码如下
-
以下的代码输出结果是什么?
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@"12", @"13", @"20", @"22", @"30", @"20", @"9"]]; for (NSString *str in array) { if ([str isEqualToString:@"20"] || [str isEqualToString:@"30"]) { [array removeObject:str]; } }
在看到这个问题的时候, 想当然就觉得, 很简单啊, 把
20和30
去掉不就是结论么?-
后来一想, 既然能作为面试题, 就不应该会这面简单, 结果敲了一遍, 果不其然挂掉了....异常如下
2016-06-15 22:51:24.674 test[24027:1499726] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x7fdd7b543e80> was mutated while being enumerated.'
这个情况还真是第一次遇到, 表示各种疑虑, 于是通过各种资料搜索, 找到了原因
-
-
问题所在
当我尝试着用
for (int i = 0; i < array.count; i++)
的方式再次尝试, 发现并没有这种问题同时, 使用
enumerateObjectsUsingBlock
也没有出现任何问题-
这时查看了一下有关NSEnumerator的文档, 发现苹果有以下几点声明:
The enumerator raises an exception if you modify the collection while enumerating 这句话说明苹果是不建议使用NSFastEnumeration的时候, 一边遍历一边修改集合 ou send "nextObject" repeatedly to a newly created NSEnumerator object to have it return the next object in the original collection. When the collection is exhausted, nil is returned. You cannot “reset” an enumerator after it has exhausted its collection. To enumerate a collection again, you need a new enumerator. 这句话要注意, nextObject是一个对象方法, 他的作用是取出集合中的一个元素后, 通过这个方法 取出并返回"原始集合"中的下一个元素, 当集合遍历结束后, 返回nil. 并且你在遍历的过程中, 不能 重新设置遍历器, 直到这个集合遍历结束 It is not safe to modify a mutable collection while enumerating through it. Some enumerators may currently allow enumeration of a collection that is modified, but this behavior is not guaranteed to be supported in the future. 从上述看出来, 当使用快速遍历器的时候, 修改一个可变的集合, 是相当不安全的操作, 可能造成一些不可预知的问题
-
综上所述, 我们可以得出一个结论
- newObject方法, 是导致上述Crash的关键所在, 从这点可以推断出, 在快速遍历器的内部有一个内置的计数器, 就像普通的for循环一下, 只不过他的这个内置计数器是不会动态改变的, 当你的数组做出修改后, 计数器并没有相应的减少, 这样就会导致继续通过计数器获取数组, 造成数组越界
-
如何避免
首先, 我们可以使用最常规的方法去遍历并且修改数据
-
通过代码我们可以发现, 由于
i < array.count
这个限制的存在, 计数器每次都会重新获取, 这样就不会导致数组越界for (int i = 0; i < array.count; i++) { NSString *str = array[i]; NSLog(@"外%li", array.count); if ([str isEqualToString:@"20"] || [str isEqualToString:@"30"]) { [array removeObject:str]; NSLog(@"内%li", array.count); } } 2016-06-15 23:30:50.162 test[24679:1526838] 外7 2016-06-15 23:30:50.162 test[24679:1526838] 外7 2016-06-15 23:30:50.163 test[24679:1526838] 外7 2016-06-15 23:30:50.163 test[24679:1526838] 内5 2016-06-15 23:30:50.163 test[24679:1526838] 外5 2016-06-15 23:30:50.163 test[24679:1526838] 内4
其次, 我们也可以使用苹果推荐的遍历器
enumerateObjectsUsingBlock
来进行遍历-
这个遍历器会在block给出的参数中, 获取数组的元素个数, 并且也不会出现上述问题
[array enumerateObjectsUsingBlock:^(NSString *str, NSUInteger i, BOOL * _Nonnull stop) { NSLog(@"外%li", array.count); if ([str isEqualToString:@"20"] || [str isEqualToString:@"30"]) { [array removeObject:str]; NSLog(@"内%li", array.count); } }];
经过同事提醒, 有一个另外的方法, 就是使用copy, 将内容复制一份, 然后再去判断不符合标准的元素, 将其从我们最开始的数组中删除
这个操作的缺点就在于我们需要先开辟内存再复制一份, 如果是较多的数据, 会造成一定的内存压力
但是对于小数据来说, 并没有任何问题
-
注意一定要在使用完毕之后释放掉, 回收内存
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@"12", @"13", @"20", @"22", @"30", @"20", @"9"]]; NSArray *array2 = [array copy]; for (NSString *str in array2) { if ([str isEqualToString:@"20"] || [str isEqualToString:@"30"]) { [array removeObject:str]; NSLog(@"内%li", array.count); } } array2 = nil;
给大家一点小建议, 在工作过程中, 可以多看看一些面试题, 里面会有各个公司工程师出的刁钻问题, 对于闲暇时间不妨为一个提升自己的小便利