关于c#:集合被修改;

Collection was modified; enumeration operation may not execute - why?

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

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


从IEnumerable文档中:

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

我相信这一决定的理由是,不能保证所有类型的集合都能保持修改,并且仍然保持枚举器状态。考虑一个链接列表——如果删除一个节点,并且枚举器当前位于该节点上,则节点引用可能是它的唯一状态。一旦该节点被删除,"下一个节点"引用将被设置为null,有效地使枚举器状态无效,并防止进一步枚举。

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


怎么

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

开始枚举时,将获取版本号的快照。每次循环期间,都会将此版本号与集合的版本号进行比较,如果它们不同,则会引发此异常。

为什么?

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


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

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

例如。。

1
2
3
4
5
6
7
8
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采用这种方法。例如:

1
var newSequence = sequence.Where(item => item < 20); // returns new sequence


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

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

请注意,修改项目是完全正确的,只有添加/删除是不正确的。


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


这是指定的行为:

Enumerators can be used to read the
data in the collection, but they
cannot be used to modify the
underlying collection.

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


您看到这是因为基础集合返回的IEnumerator可能以只读方式公开当前属性。一般来说,您应该避免对每个集合使用的集合进行更改(实际上大多数情况下,您甚至无法更改集合)。