我正在枚举实现IList的集合,在枚举期间我正在修改集合。我收到错误,“收集被修改;枚举操作可能无法执行。”
我想知道为什么在迭代期间修改集合中的项时会发生此错误。我已经将foreach循环转换为for循环,但我想知道发生此错误的原因的“细节”。
答案 0 :(得分:17)
只要集合保持不变,枚举器仍然有效。如果对集合进行了更改,例如添加,修改或删除元素,则枚举数将无法恢复,并且其行为未定义。
我认为这个决定的原因是无法保证所有类型的集合都可以维持修改并仍然保留枚举器状态。考虑一个链表 - 如果删除一个节点并且一个枚举器当前在该节点上,则该节点引用可能是其唯一的状态。删除该节点后,“下一个节点”引用将设置为null
,从而有效地使枚举器状态无效并阻止进一步枚举。
由于某些集合实现在这种情况下会遇到严重问题,因此决定将此部分与IEnumerable接口合同。在某些情况下允许修改而不是在其他情况下进行修改会非常令人困惑。此外,这意味着在更改集合实现时,可能依赖于在枚举集合时修改集合的现有代码会出现严重问题。因此,优先考虑所有可用数据的行为一致。
答案 1 :(得分:10)
在内部,大多数基本集合类都维护版本号。无论何时添加,删除,重新排序等集合,此版本号都会递增。
开始枚举时,会拍摄版本号的快照。每次循环时,都会将此版本号与集合进行比较,如果它们不同,则抛出此异常。
虽然可以实现IList
,以便它可以正确处理在 foreach循环中的集合的更改(通过让枚举器跟踪集合的更改),在枚举期间正确处理其他线程对集合所做的更改是一项更加困难的任务。因此,存在此异常有助于识别代码中的漏洞,并提供某种关于其他线程操作带来的任何潜在不稳定性的早期警告。
答案 2 :(得分:4)
虽然其他人已经描述了为什么它是无效的行为,但他们还没有提出解决问题的方法。虽然您可能不需要解决方案,但无论如何我都会提供解决方案。
如果您想观察与您正在迭代的集合不同的集合,则必须返回一个新集合。
例如..
IEnumerable<int> sequence = Enumerable.Range(0, 30);
IEnumerable<int> newSequence = new List<int>();
foreach (var item in sequence) {
if (item < 20) newSequence.Add(item);
}
// now work with newSequence
这就是你应该'修改'集合的方式。当您想要修改序列时,LINQ采用这种方法。例如:
var newSequence = sequence.Where(item => item < 20); // returns new sequence
答案 3 :(得分:1)
我假设原因是枚举器对象的状态与集合的状态有关。例如,列表枚举器将有一个int字段来存储当前元素的索引。但是,如果从列表中删除元素,则在元素保留为1之后将所有索引移位。此时,枚举器将跳过一个对象,从而表现出错误的行为。使枚举器对所有可能的情况都有效将需要复杂的逻辑并且可能损害最常见情况的性能(不改变集合)。我相信这就是为什么.NET中的集合的设计者决定他们应该在枚举器处于invald状态时抛出异常,而不是试图修复它。
答案 4 :(得分:1)
此方案中的真正技术原因是因为列表包含名为“version”的私有成员。每次修改 - 添加/删除 - 都会增加版本。 GetEnumerator返回的枚举器在创建时存储版本,并在每次调用Next时检查Version - 如果它不相等,则抛出异常。
对于内置List<T>
类以及可能用于其他集合的情况也是如此,因此如果您实现自己的IList(而不是仅在内部子类化/使用内置集合),那么您可以解决这个问题。 ,但一般来说,枚举和mofication应该在一个向后的for循环中或使用一个辅助List进行,具体取决于场景。
请注意,修改项目完全没问题,只有添加/删除不是。
答案 5 :(得分:0)
您看到这个的原因是底层集合返回的IEnumerator可能会将当前属性公开为只读。通常,您应该避免使用for-each对集合进行更改(实际上大多数情况下您甚至无法更改集合)。
答案 6 :(得分:0)