Enumerator.MoveNext() throws 'Collection was Modified' on first call
考虑以下代码:
1 2 3 4 5 | List<int> list = new List<int>(); IEnumerable<int> enumerable = list; IEnumerator<int> enumerator = enumerable.GetEnumerator(); list.Add(1); bool any = enumerator.MoveNext(); |
在运行时,最后一行抛出:
InvalidOperationException: Collection was modified; enumeration operation may not execute.
号
我理解当
为什么
可能是因为规则"如果修改了基础集合,则枚举器无效"比规则"如果在第一次调用moveNext之后修改了基础集合,则枚举器无效"。或者只是它的实现方式。另外,可以合理地假定枚举器表示创建枚举器时基础集合的状态,并且依赖不同的行为可能是错误的来源。
我觉得需要快速回顾一下迭代器。
迭代器(IEnumerator和IEnumerable for C)用于按顺序访问结构的元素,而不公开底层表示。其结果是,它允许您具有如下的外部通用函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V> { foreach (V value in collection) actor(value); } //Or the more verbose way void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V> { using (var iterator = collection.GetEnumerator()) { while (iterator.MoveNext()) actor(iterator.Current); } } //Or if you need to support non-generic collections (ArrayList, Queue, BitArray, etc) void Iterator<T, V> (T collection, Action<V> actor) where T : IEnumerable { foreach (object value in collection) actor((V)value); } |
如C规范所示,存在权衡。
5.3.3.16 foreach语句
foreach ( type identifier in expr ) embedded-statement
The definite assignment state of v at the beginning of expr is the same as the state of v at the beginning of stmt.
The definite assignment state of v on the control flow transfer to embedded-statement or to the end point of stmt is the same as the
state of v at the end of expr.
号
这只意味着值是只读的。为什么它们是只读的?很简单。由于
但这不是你最初的问题。在访问第一个元素之前,您正在修改集合,并引发了一个错误。为什么?为此,我使用ilspy挖掘了list类。这是列表类的一个片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable { private int _version; public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator { private List<T> list; private int version; private int index; internal Enumerator(List<T> list) { this.list = list; this.version = list._version; this.index = 0; } /* All the implemented functions of IEnumerator<T> and IEnumerator will throw a ThrowInvalidOperationException if (this.version != this.list._version) */ } } |
号
枚举器初始化为父列表的"版本"和对父列表的引用。所有迭代操作都会进行检查,以确保初始版本与引用列表的当前版本相同。如果它们不同步,则迭代器不再有效。为什么BCL会这样做?为什么实现人员不检查枚举器的索引是否为0(表示新的枚举器),如果是,只需重新同步版本?我不确定。我只能假设团队需要实现IEnumerable的所有类之间的一致性,他们也希望保持简单。因此,列表的枚举器(我相信大多数其他枚举器)不区分元素,只要它们在范围内。
这是你问题的根本原因。如果您绝对必须具有此功能,那么您将必须实现自己的迭代器,最后可能不得不实现自己的列表。在我看来,太多的工作阻碍了BCL的发展。
在设计BCL团队可能遵循的迭代器时,这里引用了GOF的一句话:
It can be dangerous to modify an aggregate while you're traversing it.
If elements are added or deleted from the aggregate, you might end up
accessing an element twice or missing it completely. A simple
solution is to copy the aggregate and traverse the copy, but that's
too expensive to do in general
号
BCL团队很可能认为它在时空复杂性和人力方面过于昂贵。这种哲学贯穿于整个C。允许修改foreach中的变量可能太贵了,让list的枚举器区分它在列表中的位置太贵了,而把用户放在摇篮中太贵了。希望我已经很好地解释了它,我们可以看到迭代器的强大和约束。
参考文献:
是什么改变了列表的"版本",从而使所有当前的枚举器失效?
- 通过索引器更改元素
- 埃多克斯1〔8〕
- 埃多克斯1〔9〕
- 埃多克斯1〔10〕
- 江户十一〔11〕。
- 埃多克斯1〔12〕
- 埃多克斯1〔13〕
- 埃多克斯1〔14〕
- 埃多克斯1〔15〕
- 江户十一〔16〕号
- 埃多克斯1〔17〕
这是因为在