集合已修改;枚举操作可能无法执行 - 为什么?
我正在枚举一个实现 IList 的集合,并且在枚举期间我正在修改该集合。我收到错误“集合已修改;枚举操作可能无法执行。”
我想知道为什么在迭代期间修改集合中的项目时会出现此错误。我已经将 foreach 循环转换为 for 循环,但我想知道发生此错误的“详细信息”。
I'm enumerating over a collection that implements IList, and during the enumeration I am modifying the collection. I get the error, "Collection was modified; enumeration operation may not execute."
I want to know why this error occurs when modifying a item in the collection during iteration. I've already converted my foreach loop to a for loop, but I want to know the 'details' on why this error occurs.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
来自 IEnumerable 文档:
我认为做出此决定的原因是,不能保证所有类型的集合都可以承受修改并仍然保留枚举器状态。考虑一个链表——如果删除一个节点并且枚举器当前位于该节点上,则节点引用可能是其唯一状态。一旦该节点被删除,“下一个节点”引用将被设置为 null,从而有效地使枚举器状态无效并阻止进一步枚举。
由于某些集合实现在这种情况下会遇到严重问题,因此决定将这部分纳入 IEnumerable 接口契约中。在某些情况下允许修改而在其他情况下不允许修改会非常混乱。此外,这意味着在枚举集合时可能依赖于修改集合的现有代码在集合实现发生更改时会出现严重问题。因此,最好使所有可枚举的行为保持一致。
From the IEnumerable documentation:
I believe the reasoning for this decision is that it cannot be guaranteed that all types of collections can sustain modification and still preserve an enumerator state. Consider a linked list -- if you remove a node and an enumerator is currently on that node, the node reference may be its only state. And once that node is removed, the "next node" reference will be set to
null
, effectively invalidating the enumerator state and preventing further enumeration.Since some collection implementations would have serious trouble with this kind of situation, it was decided to make this part of the IEnumerable interface contract. Allowing modification in some situations and not others would be horribly confusing. In addition, this would mean that existing code that might rely on modifying a collection while enumerating it would have serious problems when the collection implementation is changed. So making the behavior consistent across all enumerables is preferable.
如何
在内部,大多数基集合类都维护版本号。每当您添加、删除、重新排序等集合时,此版本号都会递增。
当您开始枚举时,系统会拍摄版本号的快照。每次循环时,都会将此版本号与集合的版本号进行比较,如果它们不同,则会引发此异常。
为什么
虽然可以实现
IList
,以便它可以正确处理在 foreach 循环内对集合所做的更改(通过让枚举器跟踪集合的更改),正确处理枚举期间其他线程对集合所做的更改是一项更加困难的任务。因此,此异常的存在是为了帮助识别代码中的漏洞,并针对其他线程的操作所带来的任何潜在不稳定性提供某种早期警告。How
Internally, most of the base collection classes maintain a version number. Whenever you add, remove, reorder, etc the collection, this version number is incremented.
When you start enumerating, a snapshot of the version number is taken. Each time around the loop, this version number is compared against the collection's, and if they are different then this exception is thrown.
Why
Whilst it would be possible to implement
IList
so that it could correctly deal with changes to the collection made within the foreach loop (by having the enumerator track the collection's changes), it is a much harder task to correctly deal with changes made to the collection by other threads during the enumeration. So this exception exists to help identify vulnerabilities in your code, and to provide some kind of early warning about any potential instabilities brought about by manipulation from other threads.虽然其他人已经描述了为什么这是无效的行为,但他们没有为您的问题提供解决方案。虽然您可能不需要解决方案,但无论如何我都会提供它。
如果您想要观察与您正在迭代的集合不同的集合,则必须返回一个新集合。
例如..
这就是您应该“修改”集合的方式。当您想要修改序列时,LINQ 采用这种方法。例如:
While others have described why it's invalid behaviour, they haven't offered up a solution to your problem. While you may not need a solution, I'm going to provide it anyway.
If you want to observe a collection that is different to the collection that you are iterating over, you must return a new collection.
For instance..
This is how you should be 'modifying' collections. LINQ takes this approach when you want to modify a sequence. For instance:
我认为原因是枚举器对象的状态与集合的状态相关。例如,列表枚举器将有一个 int 字段来存储当前元素的索引。但是,如果从列表中删除一个元素,则该元素后面的所有索引都会左移一位。此时,枚举器将跳过一个对象,从而表现出错误的行为。使枚举器对所有可能的情况都有效将需要复杂的逻辑,并且可能会损害最常见情况(不更改集合)的性能。我相信这就是为什么 .NET 中集合的设计者决定,当枚举器处于无效状态时,他们应该抛出异常,而不是尝试修复它。
I assume the reason is that the state of the enumerator object is related to the state of the collection. For example a list enumerator would have an int field to store the index of the current element. However if you remove an element from the list you are shifting all the indexes after the element left by one. At this point the enumerator would skip an object thus manifesting wrong behaviour. Making the enumerator valid for all possible cases would require complex logic and can hurt performance for the most common case (not changing the collection). I believe this is why the designers of the collections in .NET decided that they should just throw an exception when the enumerator is in invald state instead of trying to fix it.
这种情况下真正的技术原因是因为列表包含一个名为“version”的私有成员。每次修改 - 添加/删除 - 都会增加版本。 GetEnumerator 返回的枚举器存储创建时的版本,并在每次调用 Next 时检查版本 - 如果不相等,则会引发异常。
对于内置
List
类以及可能的其他集合来说都是如此,因此如果您实现自己的 IList(而不仅仅是在内部子类化/使用内置集合),那么您可能能够解决这个问题,但一般来说,枚举和修改应该在向后 for 循环中或使用辅助列表来完成,具体取决于场景。请注意,修改项目是完全可以的,只有添加/删除则不然。
The real technical reason in this scenario is because Lists contain a private member called "version". Every modification - Add/Remove - increments the Version. The Enumerator that GetEnumerator returns stores the version at the moment it is created and checks the Version every time Next is called - if it's not equal, it throws an exception.
This is true for the builtin
List<T>
class and possibly for other collections, so if you implement your own IList (rather than just subclassing/using a built in collection internally) then you may be able to work around that, but generally, enumeration and mofication should be done in a backwards for-loop or using a secondary List, depending on the scenario.Note that modifying an item is perfectly fine, only Add/Remove is not.
这是指定的行为:
此限制使枚举器更易于实现。
请注意,某些集合将(错误地)允许您在修改集合时进行枚举。
This is the specified behavior:
This limitation makes enumerators simpler to implement.
Note that some collections will (incorrectly) allow you to enumerate while modifying the collection.
您看到此情况的原因是基础集合返回的 IEnumerator 可能会将当前属性公开为只读。一般来说,您应该避免使用 for-each 更改集合(事实上,大多数情况下您甚至无法更改集合)。
The reason you see this is The IEnumerator returned by the underlying collection may expose the current property as read-only. Generally, you should avoid changes to collections(in fact most cases you will not even be able to change the collection) using for-each.