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

Eri*_*ura 2 .net c# collections ienumerable

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

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

if (this.version != this.list._version)
 ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
Run Code Online (Sandbox Code Playgroud)

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

然后我有一些问题:

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

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

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

Ser*_*rvy 5

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

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

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

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

当然.

考虑这个迭代器的列表:

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

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

由于an的迭代器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;
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 如果某个项目从序列中的"早期"中删除,我们将继续正常而不跳过项目.

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

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

  • 更换物品不是问题; 只会看到它是否在"当前位置之后".

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

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

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

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