Swift如何避免枚举时集合被修改?底层原理解析
一、代码行为对比
1. Swift正向/倒序遍历修改集合均正常
Swift中无论是正向还是倒序遍历可变数组时修改集合,代码都能正常执行:
正向遍历代码:
var arr = [1, 2, 3, 4, 5, 6] for num in arr { arr.removeAll { val in return val == num } } print(arr) // 输出 []
倒序遍历代码:
var arr = [1, 2, 3, 4, 5, 6] for num in arr.reversed(){ arr.removeAll { val in return val == num } } print(arr) // 输出 []
2. Objective-C正向遍历修改集合崩溃,倒序遍历正常
Objective-C中正向遍历可变数组时修改集合会直接崩溃:
崩溃代码:
NSMutableArray * arr = [NSMutableArray arrayWithObjects: @11, @22, @33, @44, @55, @66, nil]; for (NSObject * obj in arr) { [arr removeObject: obj]; } NSLog(@"%@", arr);
崩溃信息:
Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x> was mutated while being enumerated.'
但倒序遍历的变体代码可以正常运行:
NSMutableArray * arr = [NSMutableArray arrayWithObjects: @11, @22, @33, @44, @55, @66, nil]; for (NSObject * obj in arr.reverseObjectEnumerator) { [arr removeObject: obj]; } NSLog(@"%@", arr); // 输出 []
二、核心问题
为什么Swift在枚举集合时修改原集合不会崩溃,而Objective-C正向遍历会崩溃?Swift的底层机制是什么?
三、底层原理分析
1. Objective-C正向遍历崩溃的原因
Objective-C的NSMutableArray是引用类型,for-in遍历依赖的NSEnumerator枚举器会直接绑定原数组的存储状态。数组内部维护了一个修改版本号,每次增删改操作都会更新这个版本号。枚举器在每次迭代时会校验版本号,如果发现版本号和枚举开始时不一致,就会抛出NSGenericException异常,阻止非法操作。
而倒序遍历使用reverseObjectEnumerator时,枚举器的遍历逻辑是从数组末尾向前取元素。每次删除末尾元素不会影响前面未遍历元素的索引位置,且枚举器的版本号校验不会触发,因此可以正常完成遍历和修改。
2. Swift避免崩溃的核心机制
Swift的Array是值类型,且遵循**写时复制(Copy-on-Write, COW)**机制,这是它避免枚举冲突的核心:
- 当执行
for num in arr时,遍历操作是对数组的只读访问,此时不会触发复制,遍历的是原数组的存储区域。 - 当在循环中调用
removeAll修改数组时,由于原存储区域正在被遍历(只读占用),Swift会自动复制一份新的存储区域,所有修改操作都在新区域上进行。 - 遍历操作始终在原存储区域上执行,修改后的数组指向新存储区域,两者完全隔离,既不会影响遍历序列,也不会触发任何冲突或崩溃。
对于arr.reversed()的倒序遍历,reversed()返回的是ReversedCollection视图,它并不复制原数组,而是通过索引映射实现反向访问。但同样基于COW机制,当修改原数组时,会触发复制,遍历仍基于原存储的索引映射进行,因此也能正常执行。
内容的提问来源于stack exchange,提问作者DNG

