2

我知道在.net 集合类型(或至少某些集合类型)中,当您对其进行迭代时,不允许修改集合。

例如在 List 类中存在这样的代码:

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

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

然后我有几个问题:

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

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

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

4

5 回答 5

5

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

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

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

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

当然。

考虑这个迭代器的列表:

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

如果您从基础列表中删除当前索引处或之前的项目,则迭代时将跳过项目。如果您在当前索引处或之前添加一个项目,则该项目将被复制。但是,如果您在迭代期间添加/删除超过当前索引的项目,那么就不会有问题。我们可以试着花点时间尝试查看是否从列表中删除/添加了一个项目并相应地调整索引,但它并不总是有效,因此我们无法处理所有情况。如果我们有类似的东西,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;
    }
}
  • 如果一个项目从序列中的“早期”中删除,我们会正常继续而不跳过一个项目。

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

  • 如果一个项目从当前位置之前移动到之后,它将显示两次,但不会跳过或重复其他项目。如果一个项目从当前位置之后移动到当前位置之前,它将不会显示,但仅此而已。如果一个项目从集合中的后一个位置移动到另一个位置,则没有问题,移动将在结果中看到,如果它从较早的位置移动到另一个较早的位置,一切都很好,移动迭代器不会“看到”。

  • 更换物品不是问题;但是,只有在当前位置“之后”时才能看到它。

  • 重置集合会导致序列在当前位置优雅地结束。

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

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

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

于 2013-01-29T17:58:57.937 回答
4

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

于 2013-01-29T17:07:24.317 回答
1

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

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

JaredPar 的这篇博客文章中,他创建了一个ThreadSafeList<T>类:

该集合不再实现 IEnumerable。IEnumerable 仅在集合未在后台更改时才有效。以这种方式构建的集合无法轻松实现此保证,因此将其删除。

值得一提的是,并非所有的实现都IEnumerable不允许在枚举期间进行修改。并发集合可以,因为它们提供线程安全保证。

于 2013-01-29T17:07:02.693 回答
0

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

IEnumerable.GetEnumerator

只要集合保持不变,枚举数就保持有效。如果对集合进行了更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效,并且其行为未定义。

这避免了像 LinkedList 这样的集合的问题。假设您有一个包含 4 个节点的链表,并且您迭代到第二个节点。然后改变链表,将第二个节点移动到链表的头部,将第三个节点移动到链表的尾部。到那时,对您的枚举器进行下一步甚至意味着什么?可能的行为将是模棱两可的,不容易猜到。当您通过其接口处理对象时,您不必考虑底层类是什么,以及该类及其枚举器是否允许修改。接口说修改使枚举器无效,所以事情应该是这样的。

于 2013-01-29T17:32:05.737 回答
0

使用 yield 语句加载您要修改的元素并在事后这样做

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

于 2013-01-29T17:20:55.220 回答