views:

664

answers:

7

Can you remove an item from a List<> whilst iterating through it? Will this work, or is there a better way to do it?

My code:

foreach (var bullet in bullets)
  {
    if (bullet.Offscreen())
    {
      bullets.Remove(bullet);
    }
  }

-edit- Sorry guys, this is for a silverlight game. I didn't realise silverlight was different to the Compact Framework.

+3  A: 

Attempting to remove it within a foreach loop will throw an exception. You need to iterate through it backwards with a for loop.

for (int count = bullets.Count - 1; count >= 0; count--)
{
  if (bullets[count].Offscreen())
    {
        //bullets.Remove(bullets[count]);
        bullets.RemoveAt(count);
    }
}
Ahmad Mageed
This looks like a good idea, thanks. There will be no performance penalty from iterating backwards through the list you think?
Chris
for loops are generally considered to be fast. Traversing it in reverse shouldn't be much different than doing so normally. Additionally, there's no temporary list copying so it affects the original collection.
Ahmad Mageed
Also, i think 'removeat' would be faster than 'remove'
Chris
@Chris: agreed, it's a better choice since the index is known.
Ahmad Mageed
+2  A: 

It is better to either create a list that will contain items to remove, then remove items from the list:

List<Bullet> removedBullets = new List<Bullet>();

foreach(var bullet in bullets)
{
  if (bullet.OffScreen())
  {
   removedBullets.Add(bullet);
  }
}

foreach(var bullet in removedBullets)
{
  bullets.Remove(bullet);
}
Russell
This was my original thought, but since its a real time game, i'm not keen to be creating a new list due to possible overhead.
Chris
+14  A: 
bullets.RemoveAll(bullet => bullet.Offscreen());

Edit: To make this work as-is in silverlight, add the following extension method to your project.

Like List<T>.RemoveAll, this algorithm is O(N) where N is the length of the list as opposed to O(N*M) where M is the number of elements removed from the list. Since it's an extension method with the same prototype as the RemoveAll method found in non-Silverlight frameworks, the built-in one will be used when available, and this one used seamlessly for silverlight builds.

public static class ListExtensions
{
    public static int RemoveAll<T>(this List<T> list, Predicate<T> match)
    {
        if (list == null)
            throw new NullReferenceException();

        if (match == null)
            throw new ArgumentNullException("match");

        int i = 0;
        int j = 0;

        for (i = 0; i < list.Count; i++)
        {
            if (!match(list[i]))
            {
                if (i != j)
                    list[j] = list[i];

                j++;
            }
        }

        int removed = i - j;
        if (removed > 0)
            list.RemoveRange(list.Count - removed, removed);

        return removed;
    }
}
280Z28
Love it! Great answer :)
Russell
+1 you beat me to it! :)
Manga Lee
This IS the answer, it would be nice if everyone else could delete theirs...
Benjol
Sounds great, but removeall doesn't exist as a function for me (silverlight), even with a 'using system.linq' - i'm not sure what's wrong?
Chris
@Benjol: it's good to see other ways to achieve things. Sometimes other approaches make sense. For example, the reverse for loop pattern is extremely useful with arrays and XML documents where a RemoveAll method simply doesn't apply. It is also helpful in those scenarios when modifying the existing collection.
Ahmad Mageed
I've found a thread with people asking why removeall was removed from C# in newer versions:http://forums.silverlight.net/forums/t/19099.aspxStrange???
Chris
OK removeall isn't in silverlight. This is still a good solution anyway, worth remembering for other projects!
Chris
@Chris: Fixed to address the issue, and with the extension method you can use the code as-is and it will seamlessly support multiple frameworks.
280Z28
+10  A: 

Edit: to clarify, the question is regarding Silverlight, which apparently does not support RemoveAll on List`T. It is available in the full framework, CF, XNA versions 2.0+

You can write a lambda that expresses your removal criteria:

bullets.RemoveAll(bullet => bullet.Offscreen());

Or you can select the ones you do want, instead of removing the ones you don't:

bullets = bullets.Where(b => !b.OffScreen()).ToList();

Or use the indexer to move backwards through the sequence:

for(int i=bullets.Count-1;i>=0;i--)
{
    if(bullets[i].OffScreen())
    {
        bullets.RemoveAt(i);
    }
}
Rex M
This is very inefficient compared to `List<T>.RemoveAll` (see my answer).
280Z28
@280z28 ah yes, forgot about RemoveAll.
Rex M
Your second version suits me best (reverse iterate). RemoveAll doesn't seem to work any more (deprecated?).
Chris
@Chris RemoveAll definitely works. See http://msdn.microsoft.com/en-us/library/wdka673a.aspx
Rex M
@Chris ah ok, it was removed from the Silverlight libraries. It's available in all the other versions.
Rex M
@Chris: it's not deprecated, but it's also not listed as supported for Silverlight. Visit the link above by Rex and you can see the versions supported on the right. Silverlight isn't included. If you visit the page for the List.Count() method you will see Silverlight supports it. So apparently not all methods are available to Silverlight.
Ahmad Mageed
Ah ok. I saw it was available in CF and i figured silverlight would have it. Makes sense now.
Chris
I modified my answer to address the Silverlight problem, and it's significantly more efficient than the methods described in this answer (removed a factor in the algorithmic complexity).
280Z28
Replace list using Where() filter is a great use of self replace, but don't think you can beat that RemoveAll()
GONeale
+1  A: 

I've come across this problem before and blogged about it here.

Short version is that you can create an extension method called RemoveIf:

public void RemoveIf<T>(ICollection<T> collection, Predicate<T> match)
{
    List<T> removed = new List<T>();
    foreach (T item in collection)
    {
        if (match(item))
        {
            removed.Add(item); 
        }
    }

    foreach (T item in removed)
    {
        collection.Remove(item);
    }

    removed.Clear();
}

And then just call it with your delegate each time you need it:

RemoveIf(_Entities.Item, delegate(Item i) { return i.OffScreen(); });
Odd
+2  A: 

Iterate in "for" loop rather then iterating through foreach. This will work.

Amit
+3  A: 

Try this:

bullets.RemoveAll(bullet => bullet.Offscreen());
Manga Lee