为什么我在迭代时不应该修改集合

时间:2013-01-29 17:02:23

标签: c# .net collections ienumerable

我知道.net集合类型(或至少某些集合类型)在迭代时不允许修改集合。

例如在List类中存在如下代码:

if (this.version != this.list._version)
 ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);

但显然这是设计迭代器类的开发人员的决定,因为我可以提供一些IEnumerable的实现,至少在修改底层集合时不会抛出任何异常。

然后我有一些问题:

  • 为什么我在迭代时不应修改集合?

  • 可以创建一个支持在迭代时修改的集合,而不会有任何其他问题吗? (注意:第一个答案也可以回答这个问题)

  • 当C#编译器生成Enumerator接口实现时会考虑到这样的事情吗?

5 个答案:

答案 0 :(得分:5)

  

为什么我在迭代时不应修改集合?

迭代时可以修改一些集合,因此它不是全局性的。在大多数情况下,非常很难编写一个即使在修改底层集合时也能正常工作的有效迭代器。在许多情况下,例外是迭代器作者撑起并说他们只是不想处理它。

在某些情况下,当基础集合发生变化时,不清楚迭代器应该做什么。有些案例是明确的,但对于其他案例,不同的人会期望不同的行为。无论何时你处于这种情况,都表明存在更深层次的问题(你不应该改变你正在迭代的序列)

  

可以创建一个支持在迭代时修改的集合,而不会有任何其他问题吗? (注意:第一个答案也可以回答这个问题)

不确定

考虑这个列表的迭代器:

public static IEnumerable<T> IterateWhileMutating<T>(this IList<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

如果从基础列表中删除当前索引处或之前的项目,则迭代时将跳过项目。如果在当前索引处或之前添加项目,则将复制项目。但是如果在迭代期间添加/删除当前索引之外的项目,则不会出现问题。我们可以试着想象并尝试查看项目是否已从列表中删除/添加并相应地调整索引,但它无法始终有效,因此我们无法处理所有情况。如果我们有类似ObservableCollection的东西,那么我们可以收到添​​加/删除及其索引的通知并相应地调整索引,从而允许迭代器处理底层集合的变异(只要它不在另一个线程中)

由于ObservableCollection的迭代器可以知道添加/删除任何项目的时间以及它们的位置,因此可以相应地调整它的位置。我不确定内置迭代器是否正确处理了变异,但是这里可以处理底层集合的任何变异:

public static IEnumerable<T> IterateWhileMutating<T>(
    this ObservableCollection<T> list)
{
    int i = 0;
    NotifyCollectionChangedEventHandler handler = (_, args) =>
    {
        switch (args.Action)
        {
            case NotifyCollectionChangedAction.Add:
                if (args.NewStartingIndex <= i)
                    i++;
                break;
            case NotifyCollectionChangedAction.Move:
                if (args.NewStartingIndex <= i)
                    i++;
                if (args.OldStartingIndex <= i) //note *not* else if
                    i--;
                break;
            case NotifyCollectionChangedAction.Remove:
                if (args.OldStartingIndex <= i)
                    i--;
                break;
            case NotifyCollectionChangedAction.Reset:
                i = int.MaxValue;//end the sequence
                break;
            default:
                //do nothing
                break;
        }
    };
    try
    {
        list.CollectionChanged += handler;
        for (i = 0; i < list.Count; i++)
        {
            yield return list[i];
        }
    }
    finally
    {
        list.CollectionChanged -= handler;
    }
}
  • 如果项目从序列中的“之前”中删除,我们会正常继续而不会跳过项目。

  • 如果在序列中“更早”添加了一个项目,我们将不会显示它,但我们也不会再显示其他项目。

  • 如果项目从当前位置移动到之后将显示两次,但不会跳过或重复其他项目。如果某个项目从当前位置移动到当前位置之前,则不会显示该项目,但这就是全部。如果一个项目稍后从集合中移动到另一个点,则没有问题,并且将在结果中看到移动,如果它从较早的位置移动到另一个较早的位置,一切都很好并且移动迭代器不会“看到”它。

  • 更换商品不是问题;只会看到它是否在“当前位置之后”。

  • 重置集合会导致序列在当前位置正常结束。

请注意,此迭代器不会处理具有多个线程的情况。如果另一个线程改变了集合而另一个线程正在迭代,那么可能会发生错误的事情(跳过或重复的项目,甚至是异常,例如索引超出范围的异常)。 允许的是迭代期间的突变,其中只有一个线程,或者只有一个线程执行代码来移动迭代器或改变集合。

  

当C#编译器生成Enumerator接口实现时会考虑到这样的事情吗?

编译器生成接口实现;一个人。

答案 1 :(得分:4)

在迭代时不允许修改集合的一个重要原因是,如果集合中的元素被删除或者插入了新元素,它将抛弃迭代。 (在迭代在集合中工作的地方插入或删除了一个元素;现在的下一个元素是什么?新的停止条件是什么?)

答案 2 :(得分:1)

一个原因是线程安全。如果另一个线程正在添加到列表中,则无法保证迭代器以正确的方式从List<T>的后备数组中读取,这可能会导致重新分配到新数组。

值得注意的是,即使使用List<T>循环枚举for也表明缺乏线程安全性。

从他创建ThreadSafeList<T>班级的blog post by JaredPar

  

该集合不再实现IEnumerable。 IEnumerable仅在集合未在引擎盖下更改时才有效。用这种方式构建的集合无法轻易做出这种保证,因此它被删除了。

值得一提的是,IEnumerable的所有实现都不允许在枚举期间进行修改。 concurrent collections这样做,因为它们可以保证线程安全。

答案 3 :(得分:0)

使用yield语句加载要修改的元素,并在事后

执行此操作

如果你必须在迭代时修改一个集合(如果它可以被索引),使用for循环并将该对象与循环声明解除关联...但你要确保在循环周围使用lock语句来制作确定你是唯一一个操纵对象的人...并且你要记住你对循环的下一次传递你自己的操作......

答案 4 :(得分:0)

也许你可以这样做,但这可能是意外的行为,超出了IEnumerable和IEnumerator接口的意图。

IEnumerable.GetEnumerator

  

只要收集仍然存在,枚举器仍然有效   不变。如果对集合进行了更改,例如添加,   修改或删除元素,枚举器是不可恢复的   无效,其行为未定义。

这可以避免像LinkedList这样的集合出现问题。想象一下,你有一个包含4个节点的链表,然后你迭代到第二个节点。然后更改链接列表,其中第二个节点移动到链接列表的头部,第三个节点移动到尾部。在那一点上,与你的普查员一起做下一步甚至意味着什么?可能的行为将是模棱两可的,不容易猜到。当您通过其接口处理对象时,您不必考虑底层类是什么,以及该类及其枚举器是否容忍修改。界面说修改使枚举器无效,所以这应该是事情的表现。