collections被修改; 枚举操作可能无法执行 – 为什么?

我正在枚举实现IList的集合,在枚举期间我正在修改集合。 我收到错误,“收集已被修改;枚举操作可能无法执行。”

我想知道为什么在迭代期间修改集合中的项时会发生此错误。 我已经将我的foreach循环转换为for循环,但我想知道发生此错误的原因的’详细信息’。

从IEnumerable文档 :

只要集合保持不变,枚举器仍然有效。 如果对集合进行了更改(例如添加,修改或删除元素),则枚举数将无法恢复,并且其行为未定义。

我相信这个决定的理由是,不能保证所有类型的集合都能够维持修改并仍然保留枚举器状态。 考虑链接列表 – 如果删除节点并且当前节点上的枚举器,则节点引用可能是其唯一的状态。 删除该节点后,“下一个节点”引用将设置为null ,从而有效地使枚举器状态无效并阻止进一步枚举。

由于某些集合实现会在这种情况下遇到严重问题,因此决定将此部分与IEnumerable接口合同。 在某些情况下允许修改而不是在其他情况下进行修改会非常令人困惑。 此外,这意味着在更改集合实现时,可能依赖于在枚举集合时修改集合的现有代码会出现严重问题。 因此,优化所有枚举的行为是一致的。

怎么样

在内部,大多数基本集合类都维护版本号。 无论何时添加,删除,重新排序等集合,此版本号都会递增。

当您开始枚举时,将获取版本号的快照。 每次循环时,都会将此版本号与集合进行比较,如果它们不同,则抛出此exception。

为什么

虽然可以实现IList以便它可以正确处理foreach循环中所做集合的更改(通过让枚举器跟踪集合的更改),但正确处理对集合所做的更改是一项更加困难的任务。在枚举期间由其他线程。 因此,存在此exception有助于识别代码中的漏洞,并提供某种关于其他线程操作带来的潜在不稳定性的早期警告。

虽然其他人已经描述了为什么它是无效行为,但他们没有提供解决问题的方法。 虽然您可能不需要解决方案,但无论如何我都会提供解决方案。

如果要观察与要迭代的集合不同的集合,则必须返回新集合。

例如..

 IEnumerable sequence = Enumerable.Range(0, 30); IEnumerable newSequence = new List(); 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 

我假设原因是枚举器对象的状态与集合的状态有关。 例如,列表枚举器将有一个int字段来存储当前元素的索引。 但是,如果从列表中删除元素,则在元素保留为1之后将所有索引移位。 此时,枚举器将跳过一个对象,从而表现出错误的行为。 使枚举器对所有可能的情况都有效将需要复杂的逻辑,并且可能损害最常见情况的性能(不改变集合)。 我相信这就是为什么.NET中的集合的设计者决定他们应该在枚举器处于invald状态时抛出exception,而不是试图修复它。

这种情况下的真正技术原因是因为列表包含一个名为“version”的私有成员。 每次修改 – 添加/删除 – 都会增加版本。 GetEnumerator返回的枚举器在创建时存储版本,并在每次调用Next时检查Version – 如果它不相等,则抛出exception。

这对于内置的List类以及可能对于其他集合来说都是如此,因此如果您实现自己的IList(而不是仅在内部子类化/使用内置集合),那么您可以解决这个问题,但通常情况下,枚举和mofication应该在后向for循环中或使用辅助List进行,具体取决于场景。

请注意,修改项目非常好,只有添加/删除不是。

这是指定的行为:

枚举器可用于读取集合中的数据,但不能用于修改基础集合。

此限制使枚举器更易于实现。
请注意,某些集合将(错误地)允许您在修改集合时进行枚举。

您看到这个的原因是底层集合返回的IEnumerator可能会将当前属性公开为只读。 通常,您应该避免使用for-each对集合进行更改(实际上大多数情况下您甚至无法更改集合)。