.NET - Remove from a List<T> within a 'foreach' loop

眉间皱痕 提交于 2019-11-28 04:05:42

Do you really need to do this within a foreach loop?

This will achieve the same results as your examples, ie, remove all items from the list up until the first item that matches the condition (or remove all items if none of them match the condition).

int index = Os.FindIndex(x => x.cond);

if (index > 0)
    Os.RemoveRange(0, index);
else if (index == -1)
    Os.Clear();

You can iterate through the list backwards:

for (int i = myList.Count - 1; i >= 0; i--)
{
    if (whatever) myList.RemoveAt(i);
}

In response to your comment about wanting to quit when you find an item that you're NOT removing, then just using a while loop would be the best solution.

User

You should never remove anything from a collection you are iterating over while inside of a foreach loop. It's basically like sawing the branch you are sitting on.

Use your while alternative. It is the way to go.

Milhous

I am a Java programmer, but something like this works:

List<Type> Os;
List<Type> Temp;
...
foreach (Type o in Os)
    if (o.cond)
        Temp.add(o);
Os.removeAll(Temp);  

I just had that problem with my analysis library. I tried this:

for (int i = 0; i < list.Count; i++)
{                
   if (/*condition*/)
   {
       list.RemoveAt(i);
       i--;
   }
}

It's pretty simple but I haven't thought of any breaking point.

sonjz

Here is the EASIEST SOLUTION with the simpliest WHY

PROBLEM:

Typically, we are removing from the original list, this produces the problem of maintaining the list count and iterator location.

List<Type> Os = ....;
Os.ForEach(
    delegate(Type o) {
        if(!o.cond) Os.Remove(o);
    }
);

SOLUTION - LINQ.ForEach:

Note all I've added was ToList(). This creates a new list that you perform ForEach on, therefore you can remove your original list, yet keep iterating through the entire list.

List<Type> Os = ....;
Os.ToList().ForEach(
    delegate(Type o) {
        if(!o.cond) Os.Remove(o);
    }
);

SOLUTION - Regular foreach:

This technique also works for regular foreach statements.

List<Type> Os = ....;
foreach(Type o in Os.ToList()) {
  if(!o.cond) Os.Remove(o);
}

Please note, that this solution won't work if your original List contains struct element.

I know you asked for something else, but if you want to conditionally remove a bunch of elements you can use lambda expression:

Os.RemoveAll(o => !o.cond);
 Os.RemoveAll(delegate(int x) { return /// });

I'd try finding the index of first item that does not satisfy the predicate and do RemoveRange(0, index) on it. If nothing else, there should be less Remove calls.

Lucas

Update: Added for completeness

As several have answered, you shouldn't modify a collection while iterating it with GetEnumerator() (example foreach). The framework prevent you from doing this by throwing an exception. The generic colution to this is to iterate "manually" with for (see other answers). Be careful with your index so you don't skip items or re-evaluate the same one twice (by using i-- or iterating backward).

However, for your specific case, we can optimize the remove operation(s)... original answer below.


If what you want is to remove all items until one meets a given condition (that's what your code does), you can do this:

bool exitCondition;

while(list.Count > 0 && !(exitCondition = list[0].Condition))
   list.RemoveAt(0);

Or if you want to use a single remove operation:

SomeType exitCondition;
int index = list.FindIndex(i => i.Condition);

if(index < 0)
    list.Clear();
else
{
    exitCondition = list[0].State;
    list.RemoveRange(0, count);
}

Note: since I'm assuming that item.Condition is bool, I'm using item.State to save the exit condition.

Update: added bounds checking and saving exit condition to both examples

you can do it with linq

MyList = MyList.Where(x=>(someCondition(x)==true)).ToList()

If you know your list isn't very large you can use

foreach (Type o in new List<Type>(Os))
    ....

which will create a temporary duplicate of the list. Your remove() call will then not be interfering with the iterator.

Look at Enumerable.SkipWhile()

Enumerable.SkipWhile( x => condition).ToList()

Generally not mutating a list, makes live a lot easier. :)

Ethan Heilman

There is a good discussion of this in Removing items in a list while iterating through it .

They propose:

for(int i = 0; i < count; i++)
{
    int elementToRemove = list.Find(<Predicate to find the element>);

    list.Remove(elementToRemove);
}

Anzurio's solution is probably the most straightforward, but here's another clean one, if you don't mind adding a bunch of interfaces/classes to your utilities library.

You can write it like this

List<Type> Os;
...
var en = Os.GetRemovableEnumerator();
while (en.MoveNext())
{
    if (en.Current.Cond)
        en.Remove();
}

Put the following infrastructure, inspired by Java's Iterator<T>.remove, into your utility library:

static class Extensions
{
    public static IRemovableEnumerator<T> GetRemovableEnumerator<T>(this IList<T> l)
    {
        return new ListRemovableEnumerator<T>(l);
    }
}

interface IRemovableEnumerator<T> : IEnumerator<T>
{
    void Remove();
}

class ListRemovableEnumerator<T> : IRemovableEnumerator<T>
{
    private readonly IList<T> _list;
    private int _count;
    private int _index;
    public ListRemovableEnumerator(IList<T> list)
    {
        _list = list;
        _count = list.Count;
        _index = -1;
    }

    private void ThrowOnModification()
    {
        if (_list.Count != _count)
            throw new InvalidOperationException("List was modified after creation of enumerator");
    }
    public void Dispose()
    {
    }

    public bool MoveNext()
    {
        ThrowOnModification();
        if (_index + 1 == _count)
            return false;
        _index++;
        return true;
    }

    public void Reset()
    {
        ThrowOnModification();
        _index = -1;
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public T Current
    {
        get { return _list[_index]; }
    }

    public void Remove()
    {
        ThrowOnModification();
        _list.RemoveAt(_index);
        _index--;
        _count--;
    }
}

I just had the same problem and solved it by using the following:

foreach (Type o in (new List(Os))) { if (something) Os.Remove(o); }

It iterates through a copy of the list and removes from the original list.

Add the item to remove in a list, and then remove these items by using RemoveAll:

List<Type> Os;
List<Type> OsToRemove=new List<Type>();
...
foreach (Type o in Os){
    if (o.cond)
        return;
    else
        OsToRemove.Add(o);
}
Os.RemoveAll(o => OsToRemove.Contains(o));
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!