在 foreach 迭代集合时无法添加/删除项目。

5
如果我自己实现了IEnumerator接口,那么我就可以在foreach语句内添加或删除albumsList中的项目而不会生成异常。但是,如果foreach语句使用由albumsList提供的IEnumerator,则尝试在foreach内部添加/删除albumsList中的项目将导致异常:
class Program
{
    static void Main(string[] args)
    {

        string[] rockAlbums = { "rock", "roll", "rain dogs" };
        ArrayList albumsList = new ArrayList(rockAlbums);
        AlbumsCollection ac = new AlbumsCollection(albumsList);
        foreach (string item in ac)
        {
            Console.WriteLine(item);
            albumsList.Remove(item);  //works

        }

        foreach (string item in albumsList)
        {
            albumsList.Remove(item); //exception
        }



    }

    class MyEnumerator : IEnumerator
    {
        ArrayList table;
        int _current = -1;

        public Object Current
        {
            get
            {
                return table[_current];
            }
        }

        public bool MoveNext()
        {
            if (_current + 1 < table.Count)
            {
                _current++;
                return true;
            }
            else
                return false;
        }

        public void Reset()
        {
            _current = -1;
        }

        public MyEnumerator(ArrayList albums)
        {
            this.table = albums;
        }

    }

    class AlbumsCollection : IEnumerable
    {
        public ArrayList albums;

        public IEnumerator GetEnumerator()
        {
            return new MyEnumerator(this.albums);
        }

        public AlbumsCollection(ArrayList albums)
        {
            this.albums = albums;
        }
    }

}

a) 我假设在albumsList提供的A实现IEnumerator时,抛出异常的代码位于A内部?

b) 如果我想在遍历集合时添加/删除项,是否总是需要提供自己的IEnumerator接口实现,或者可以将albumsList设置为允许添加/删除项?

谢谢

4个回答

15

最简单的方法是像 for(int i = items.Count-1; i >=0; i--) 那样反向遍历列表,或者首先循环一次,将需要移除的所有项目收集到一个列表中,然后遍历需要移除的项目,从原始列表中移除它们。


我在搜索和修改任何类型的文本时都从后面开始的原因是一样的:下一个项目的位置不会改变。 :) - Nelson Rothermel
1
也许,for(int i=items.Count-1; ...会更好? - alcsan

13

通常不建议设计允许您在枚举时修改集合的集合类,除非您的意图是专门设计某些线程安全的内容,以便这种情况是可能的(例如,在一个线程中添加,在另一个线程中枚举)。

原因有很多。以下是其中之一。

您的MyEnumerator类通过递增内部计数器来工作。其Current属性将在ArrayList中公开给定索引处的值。这意味着枚举集合并删除“每个”项实际上无法按预期工作(即,它不会删除列表中的每个项)。

考虑以下可能性:

您发布的代码实际上将执行以下操作:

  1. 您首先递增索引到0,这会给您一个“rock”的Current。您删除了“rock”。
  2. 现在集合有["roll", "rain dogs"],您将索引递增到1,使Current等于“rain dogs”(而不是“roll”)。接下来,您删除了“rain dogs”。
  3. 现在集合只有["roll"],并且您将索引递增到2(大于Count);因此,您的枚举器认为它已经完成了。

但是,这是问题实现的其他原因。例如,使用您的代码的人可能不了解您的枚举器如何工作(也不应该),因此可能不会意识到在foreach块中调用Remove的代价会导致每次迭代都进行线性搜索——即,IndexOf的开销(请参见MSDN对ArrayList.Remove的文档以验证这一点)。

基本上,我的意思是:除非你正在设计一些线程安全的东西,否则你不想能够在 foreach 循环内部删除项目(也许会)。

那么,有什么替代方案吗?以下是一些要点:

  1. 不要设计你的集合以允许 — 更不要期望 — 在枚举过程中进行修改。它会导致奇怪的行为,比如我上面提供的例子。
  2. 相反,如果你想提供批量删除功能,考虑使用诸如 Clear(删除所有项)或 RemoveAll(删除与指定筛选器匹配的项)等方法。
  3. 这些批量删除方法可以很容易地实现。例如,ArrayList 已经有了一个 Clear 方法,大多数你可能在 .NET 中使用的集合类也是如此。否则,如果你的内部集合是索引的,一种常见的方法是使用 for 循环从顶部索引开始枚举,并在需要删除的索引上调用 RemoveAt (请注意,这样可以解决两个问题:通过从顶部向后遍历,确保访问集合中的每个项;此外,通过使用 RemoveAt 而不是 Remove,可以避免重复线性查找带来的惩罚)。
  4. 另外,我强烈建议始终避免使用非泛型集合,例如 ArrayList。相反,使用强类型、泛型的版本,例如 List(Of Album)(假设你有一个 Album 类 — 否则,List(Of String) 仍然比 ArrayList 更具类型安全性)。

说得好。如果可以的话,我会给它+2。 - Toby
只是出于好奇 - 是由albumsList提供的枚举器实际上检测到(并因此抛出异常),我们正在尝试删除/添加元素吗?无论如何,我会遵循您的指导。 - flockofcode
1
@flockofcode:是的和不是。实际上,ArrayList.GetEnunerator 的工作方式是创建一个对象,该对象引用底层的 ArrayList 对象,并维护一个数字来表示其状态。当在枚举器上调用 MoveNext 时,它会将此数字与枚举器构造时的数字进行比较,并在数字不匹配时引发异常。因此,这是 ArrayList 和枚举器之间的交换导致了异常的抛出。 - Dan Tao
很遗憾,微软没有定义IEnumerable的变体,以不同的方式处理集合修改场景。我希望至少有两个变体:IMultipassEnumerable,其枚举器将包括Reset和Count方法,并保证多次通过将产生相同的项;IModifiableEnumerable,它允许在枚举期间进行修改(虽然不一定是线程安全的,并且不能保证何时观察到修改),而不会破坏枚举器,以及ThreadSafeModifiableEnumerator。 - supercat

0
假设我有一个集合,一个数组来说
int[] a = { 1, 2, 3, 4, 5 };

我有一个函数

   public IList<int> myiterator()
        {
            List<int> lst = new List<int>();
            for (int i = 0; i <= 4; i++)
            {
                lst.Add(a[i]);
            }

              return lst;
        }

现在我调用这个函数并迭代尝试添加

   var a = myiterator1();
    foreach (var a1 in a)
       {
         a.Add(29);
       }

会导致运行时异常

需要注意的是,如果我们允许为列表中的每个元素添加

列表将变成类似于{1,2,3,4,5,6}的东西, 然后对于每个元素和每个新添加的元素,我们都会继续添加,因此我们将陷入无限操作中,因为它将再次重复每个元素。


-1
INotifyCollectionChanged的MSDN文档中可以得知:
您可以枚举任何实现IEnumerable接口的集合。但是,为了设置动态绑定,使得集合中的插入或删除操作自动更新UI,该集合必须实现INotifyCollectionChanged接口。该接口公开了CollectionChanged事件,每当基础集合更改时都必须引发该事件。
WPF提供了ObservableCollection<(Of <(T)>)>类,它是一个内置的数据集合实现,可公开INotifyCollectionChanged接口。有关示例,请参见“How to: Create and Bind to an ObservableCollection”。
集合中的各个数据对象必须满足绑定源概述中描述的要求。
在实现自己的集合之前,请考虑使用ObservableCollection<(Of <(T)>)>或其中一个现有的集合类,例如List<(Of <(T)>)>、Collection<(Of <(T)>)>和BindingList<(Of <(T)>)>等。
如果您有高级场景并想要实现自己的集合,请考虑使用IList,它提供了一个非泛型对象集合,可以通过索引单独访问,并提供最佳性能。
听起来问题在于集合本身,而不是它的枚举器。

这与问题有任何相关性吗? - LukeH
是的。如果您正在迭代的集合没有实现INotifyCollectionChanged,则尝试在foreach块内进行修改将引发异常。这描述了问题的原因,并指出了如何解决它。 - Craig Trader
@W. Craig Trader:我认为LukeH的问题源于没有明显证据表明OP正在询问与UI的交互。此外,实现INotifyCollectionChanged并不能像魔法一样让你在foreach块内添加/删除元素。 - Dan Tao
是的。如果您正在迭代的集合没有实现INotifyCollectionChanged接口,那么尝试在foreach块内部进行修改将会抛出异常。但是,集合AlbumsCollection没有实现INotifyCollectionChanged接口,但是foreach块并没有抛出异常。 - flockofcode
我认为LukeH的问题源于没有明确的证据表明OP正在询问与UI的交互。我最近才开始学习编程,所以我还没有涉及任何UI技术(如Asp.Net)。 - flockofcode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接