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

我有一个典型的例子,试图从集合中移除一个项,同时在循环中枚举它:

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,因为枚举器不喜欢底层集合更改。

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

  1. 不要在这个循环中删除,而是保留一个单独的“删除列表”,在主循环之后处理。

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

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

    这个 可以满足我所有的需求,但是它意味着一个 很多代码必须改变,以便每次访问一个项目时检查非活动标志。我可不喜欢这种管理方式。

  3. 以某种方式将模式2的思想合并到源自 List<T>的类中。此 Superlist 将处理非活动标志、事后删除对象,并且不会向枚举使用者公开标记为非活动的项。基本上,它只是封装了模式2(以及随后的模式1)的所有思想。

    这样的类存在吗? 有人有这样的代码吗? 还是有更好的方法?

  4. 我被告知访问 myIntCollection.ToArray()而不是 myIntCollection将解决这个问题,并允许我在循环中删除。

    这看起来像是一个糟糕的设计模式,或者也许它的罚款?

详情:

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

  • 在循环中,我将执行各种过程,添加、删除等,因此解决方案需要相当通用。

  • 我需要删除 也许不会的项目是循环中的当前项目。例如,我可能在一个30项循环的第10项上,需要删除第6项或第26项。由于这个原因,向后遍历数组将不再有效。;

114292 次浏览

The best solution is usually to use the RemoveAll() method:

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

Or, if you need certain elements removed:

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

This assume that your loop is solely intended for removal purposes, of course. If you do need to additional processing, then the best method is usually to use a for or while loop, since then you're not using an enumerator:

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

Going backwards ensures that you don't skip any elements.

Response to Edit:

If you're going to have seemingly arbitrary elements removed, the easiest method might be to just keep track of the elements you want to remove, and then remove them all at once after. Something like this:

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));

If you must both enumerate a List<T> and remove from it then I suggest simply using a while loop instead of a foreach

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

When you need to iterate through a list and might modify it during the loop then you are better off using a for loop:

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

Of course you must be careful, for example I decrement i whenever an item is removed as otherwise we will skip entries (an alternative is to go backwards though the list).

If you have Linq then you should just use RemoveAll as dlev has suggested.

How about

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

As you enumerate the list, add the one you want to KEEP to a new list. Afterward, assign the new list to the 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;

I know this post is old, but I thought I'd share what worked for me.

Create a copy of the list for enumerating, and then in the for each loop, you can process on the copied values, and remove/add/whatever with the source list.

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

Let's add you code:

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

If you want to change the list while you're in a foreach, you must type .ToList()

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

If you're interested in high performance, you can use two lists. The following minimises garbage collection, maximises memory locality and never actually removes an item from a list, which is very inefficient if it's not the last item.

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;
}

For those it may help, I wrote this Extension method to remove items matching the predicate and return the list of removed items.

    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;
}

Just figured I'll share my solution to a similar problem where i needed to remove items from a list while processing them.

So basically "foreach" that will remove the item from the list after it has been iterated.

My test:

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));


list.PopForEach((item) =>
{
Console.WriteLine($"Process {item.Name}");
});


Assert.That(list.Count, Is.EqualTo(0));

I solved this with a extension method "PopForEach" that will perform a action and then remove the item from the list.

public static class ListExtensions
{
public static void PopForEach<T>(this List<T> list, Action<T> action)
{
var index = 0;
while (index < list.Count) {
action(list[index]);
list.RemoveAt(index);
}
}
}

Hope this can be helpful to any one.

Currently you are using a list. If you could use a dictionary instead, it would be much easier. I'm making some assumptions that you are really using a class instead of just a list of ints. This would work if you had some form of unique key. In the dictionary, object can be any class you have and int would be any unique key.

    Dictionary<int, object> myIntCollection = new Dictionary<int, object>();
myIntCollection.Add(42, "");
myIntCollection.Add(12, "");
myIntCollection.Add(96, "");
myIntCollection.Add(25, "");




foreach (int i in myIntCollection.Keys)
{
//Check to make sure the key wasn't already removed
if (myIntCollection.ContainsKey(i))
{
if (i == 42) //You can test against the key
myIntCollection.Remove(96);
if (myIntCollection[i] == 25) //or you can test against the value
myIntCollection.Remove(42);
}
}

Or you could use

    Dictionary<myUniqueClass, bool> myCollection; //Bool is just an empty place holder

The nice thing is you can do anything you want to the underlying dictionary and the key enumerator doesn't care, but it also doesn't update with added or removed entries.