Enumerator.MoveNext()在第一次调用时抛出'Collection was Modified'

时间:2012-07-02 02:20:15

标签: c# .net multithreading iterator ienumerable

请考虑以下代码:

List<int> list = new List<int>();
IEnumerable<int> enumerable = list;
IEnumerator<int> enumerator = enumerable.GetEnumerator();
list.Add(1);
bool any = enumerator.MoveNext();

在运行时,最后一行抛出:

  

InvalidOperationException:修改了集合;枚举操作可能无法执行。

我理解当IEnumerators发生变化时,IEnumerable需要抛出'修改后的集'异常,但我不明白这一点:

为什么IEnumerator会在MoveNext()第一次调用中抛出此异常?由于IEnumerator在第一次调用IEnumerable之前不代表MoveNext()的状态,为什么它不能开始跟踪第一个MoveNext()的更改来自GetEnumerator()

3 个答案:

答案 0 :(得分:8)

可能是因为规则“如果基础集合被修改,则枚举器无效”比规则“如果在第一次调用MoveNext后修改基础集合时枚举器无效”则更简单。或者只是它的实施方式。另外,假设Enumerator在创建Enumerator时表示底层集合的状态是合理的,并且依赖于不同的行为可能是错误的来源。

答案 1 :(得分:5)

我觉得需要快速回顾一下迭代器。

迭代器(IEnumerator和用于C#的IEnumerable)用于以有序的方式访问结构的元素,而不暴露底层表示。结果是它允许您具有可扩展的通用功能,如下所示。

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(expr中的类型标识符)embedded-statement

     
      
  • expr开头的v的明确赋值状态与stmt开头的v的状态相同。

  •   
  • 控制流转移到嵌入式语句或stmt的结束点的v的明确赋值状态与   expr结束时的v状态。

  •   

这意味着值是只读的。他们为什么只读?这很简单。由于foreach是如此高级别的声明,因此它不能也不会假设您正在迭代的容器。如果您在迭代二叉树并决定在foreach语句中随机分配值,该怎么办?如果foreach没有强制执行只读访问,那么您的二叉树将退化为树。整个数据结构将处于混乱状态。

但这不是你原来的问题。您甚至在访问第一个元素之前修改了集合,并且抛出了错误。为什么?为此,我使用ILSpy挖掘了List类。这是List类的片段

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的类中保持一致,他们也希望保持简单。因此,List的枚举器(我相信大多数其他人)只要它们在范围内就不会区分元素。

这是您问题的根本原因。如果您绝对必须具有此功能,那么您将必须实现自己的迭代器,并且最终可能必须实现自己的List。在我看来,对BCL的流动起了太多的作用。

在设计BCL团队可能遵循的迭代器时,这是GoF的引用:

  

在遍历聚合时修改聚合可能很危险。   如果从聚合中添加或删除元素,则可能最终结束   访问元素两次或完全错过它。一个简单的   解决方案是复制聚合并遍历副本,但那就是   一般来说太贵了

BCL团队很可能认为时空复杂性和人力太贵。整个C#都可以看到这种理念。允许修改foreach中的变量可能太昂贵了,使List的Enumerator区分它在列表中的位置太昂贵,而且太昂贵了。希望我已经很好地解释了它可以看到迭代器的力量和约束。

<强>参考

什么更改了列表的“版本”,从而使所有当前的枚举器无效?

  • 通过索引器更改元素
  • Add
  • AddRange
  • Clear
  • Insert
  • InsertRange
  • RemoveAll
  • RemoveAt
  • RemoveRange
  • Reverse
  • Sort

答案 2 :(得分:1)

这是因为version中有一个私人List<T>字段,会在调用MoveNext时进行检查。所以现在我们知道如果我们有一个实现MyList<T>的自定义IEnumerable<T>,我们可以避免检查version,并且允许枚举甚至修改集合(但它可能会导致意外行为)。 / p>