在C#中枚举时从List <T>中删除项目的智能方式


86

我有一个经典的案例,尝试在循环中枚举项目时从集合中删除它:

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)
{
    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.
}

在发生更改后的迭代开始时,InvalidOperationException会抛出an ,因为枚举器不喜欢基础集合的更改。

我需要在迭代时对集合进行更改。有许多模式可以用来避免这种情况,但是似乎没有一个好的解决方案:

  1. 不要在此循环内删除,而要保留一个单独的“删除列表”,在主循环后进行处理。

    通常,这是一个很好的解决方案,但就我而言,我需要立即“等待”该项目,直到真正删除该项目的主循环改变了我的代码的逻辑流程。

  2. 无需删除该项目,只需在该项目上设置一个标志并将其标记为非活动即可。然后添加模式1的功能以清理列表。

    满足我的所有需求,但是这意味着每次访问一个项目时,为了检查非活动标志,必须更改许多代码。按我的喜好,这太过分了。

  3. 以某种方式将模式2的思想纳入源自的类中List<T>。此超级列表将处理非活动标志,事实结束后的对象删除,也不会向枚举使用者显示标记为非活动的项目。基本上,它只是封装了模式2(以及随后的模式1)的所有思想。

    是否存在这样的类?有人为此提供代码吗?或者,还有更好的方法?

  4. 有人告诉我,访问myIntCollection.ToArray()而不是myIntCollection可以解决问题,并允许我在循环内删除。

    对我来说,这似乎是一种糟糕的设计模式,或者还好吗?

细节:

  • 该列表将包含许多项目,我将仅删除其中一些。

  • 在循环内部,我将进行各种各样的过程,包括添加,删除等,因此解决方案需要相当通用。

  • 我需要删除的项目可能不是循环中的当前项目。例如,我可能处于30个项目循环的项目10上,需要删除项目6或项目26。因此,向后遍历数组将不再起作用。; o(


可能对其他人有用的信息:避免Collection已修改错误(模式1的封装)
George Duckett

旁注:列出多余的时间(通常为O(N),其中N是列表的长度)来移动值。如果确实需要有效的随机访问,则可以使用平衡的二叉树来保存O(log N)中的删除,该二叉树保留了其根为子树的节点数。它是一个BST,它的键(序列中的索引)是隐含的。
Palec

Answers:


195

最好的解决方案通常是使用以下RemoveAll()方法:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

或者,如果您需要删除某些元素:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

当然,这假定您的循环仅用于删除目的。如果确实需要其他处理,那么最好的方法通常是使用fororwhile循环,因为那样的话您就不会使用枚举器:

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

向后移动可确保您不跳过任何元素。

回应编辑

如果要删除看似任意的元素,最简单的方法可能是只跟踪要删除的元素,然后立即将其全部删除。像这样:

List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    {
        toRemove.Add(elem);
    }
}

// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));

关于您的回复:我需要在处理该项目的过程中立即删除这些项目,而不是在处理完整个循环之后。我正在使用的解决方案是将要立即删除的所有项目都设为NULL,然后再删除它们。这不是理想的解决方案,因为我必须到处检查NULL,但是它确实可以工作。
约翰·

想知道如果'elem'不是一个int,那么我们不能使用RemoveAll那样将其存在于已编辑的响应代码中。
用户M

22

如果您必须同时枚举aList<T>并将其从中删除,那么我建议您仅使用while循环而不是aforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

我认为这应该是公认的答案。这样,您就可以考虑列表中的其余项目,而无需重复列出要删除的项目的简短列表。
Slvrfn

13

我知道这篇文章很旧,但是我想分享一下对我有用的东西。

创建列表的副本以进行枚举,然后在for每个循环中,您可以处理复制的值,并使用源列表删除/添加/随便什么。

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

“ .ToList()”上的好主意!简单的拍板,也可以在不直接使用股票“ Remove ...()”方法的情况下使用。
galaxis

1
虽然效率很低。
nawfal

8

当您需要遍历列表并可能在循环中对其进行修改时,最好使用for循环:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

当然,您必须小心,例如,i每当删除一项时我都会递减,否则我们将跳过条目(另一种方法是通过列表后退)。

如果您有Linq,则应RemoveAll按照dlev的建议使用。


不过,仅在删除当前元素时有效。如果要删除任意元素,则需要检查其索引是否在当前索引之前/之前或之后,以确定是否要--i
CompuChip

最初的问题没有明确指出必须支持删除除当前元素以外的其他元素@CompuChip。自得到澄清以来,这个答案没有改变。
Palec

@Palec,我了解,因此我要发表评论。
CompuChip

5

枚举列表时,将要保留的列表添加到新列表中。然后,将新列表分配给myIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)
{
    if (i want to delete this)
        ///
    else
        newCollection.Add(i);
}
myIntCollection = newCollection;

3

让我们添加代码:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

如果要在foreach中更改列表,则必须输入 .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

0

怎么样

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

在当前的C#中,可以将其重写foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }为任何枚举,List<T>甚至在.NET 2.0中也特别支持此方法。
Palec'Mar

0

如果您对高性能感兴趣,可以使用两个列表。以下代码可以最大程度地减少垃圾回收,最大化内存局部性,并且从不实际从列表中删除项目,如果它不是最后一项,则效率很低。

private void RemoveItems()
{
    _newList.Clear();

    foreach (var item in _list)
    {
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    }

    var swap = _list;
    _list = _newList;
    _newList = swap;
}

0

对于那些可能有用的方法,我编写了此Extension方法来删除与谓词匹配的项目,并返回已删除项目的列表。

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.