views:

1403

answers:

5

I need to remove multiple items from a Dictionary. A simple way to do that is as follows :

  List<string> keystoremove= new List<string>();
  foreach (KeyValuePair<string,object> k in MyCollection)
     if (k.Value.Member==foo)
        keystoremove.Add(k.Key);
  foreach (string s in keystoremove)
        MyCollection.Remove(s);

The reason why I can't directly Remove the items in the foreach block is that this would throw an Exception ("Collection was modified...")

I'd like to do the following :

 MyCollection.RemoveAll(x =>x.Member==foo)

But the Dictionary<> class doesn't expose a RemoveAll(Predicate<> Match) method, like the List<> Class does.

What's the best way (both performance wise and elegant wise) to do that?

A: 

Can you just change your loop to use an index (i.e. FOR instead of FOREACH)? You'd have to loop backwards, of course, i.e. count-1 down to zero.

Geoff
You can't iterate through a Dictionary with FOR.
Brann
Sorry, I thought the extension .ElementAt(index) would allow this, at least in .net 3.5.
Geoff
+12  A: 

Here's an alternate way

foreach ( var s in MyCollection.Where(p => p.Value.Member == foo).ToList() ) {
  MyCollection.Remove(s.Key);
}

Pushing the code into a list directly allows you to avoid the removing while enumerating problem. The .ToList() will force the enumeration before the foreach really starts.

JaredPar
Good answer, but I don't think the ToList() is required. Is it?
Jim Mischel
Good answer, but If I'm not mistaking, s is an instance of the Value type, so the ending s.key won't compile, or will it?
Brann
I don't think I like this solution too much because of the ".ToList()". Its there for a purpose, but its not self-evident what its purpose is until you remove .ToList() and observe the error yourself. I wouldn't recommend this code when there are more readable alternatives available.
Juliet
@JaredPar, please fix your answer, it is funny to see that most voted answer won't even compile. Also it is not necessary to query over values, you can get keys directly
aku
I'm waiting for someone with editing rights to fix this before I mark this post as the accepted answer.
Brann
@Jim, the .ToList is absolutely necessary here. The foreach body modifies the underlying collection. Without the .ToList() the Where clause will be operating against a modified collection. Using .ToList() forces the query to complete before any remove occurs
JaredPar
@aku, fixed the typo. I originally coded it a different way and then later came back and wanted to use KeyValuePairs. I forgot to update the selection.
JaredPar
@Princess, this unfortunately is just life with the implementation of IEnumerable. Yes it's a bit awkward the first time you see it but once you hit the pattern once it's not as bad.
JaredPar
@Brann, it should be good now
JaredPar
+1  A: 

Instead of removing just do the inverse (create a new dictionary from the old one containing only the elements you are interested in) and let the garbage collector take care of the old dictionary:

var newDictionary = oldDictionary.Select(x => x.Value != foo);
Darin Dimitrov
This would potentially lead to terrible performance, wouldn't it?
Brann
With the var keyword in there, would this not give newDictionary a type of IEnumerable<KeyValuePair<TKey, TValue>>, rather than Dictionary<TKey, TValue> ?
Chris Ammerman
Enumerable.Select does not do filtering.
David B
+6  A: 

you can create extension method:

public static class DictionaryExtensions
{
    public static void RemoveAll<TKey, TValue>(this Dictionary<TKey, TValue> dic, 
        Func<TValue, bool> predicate)
    {
        var keys = dic.Keys.Where(k => predicate(dic[k])).ToList();
        foreach (var key in keys)
        {
            dic.Remove(key);
        }
    }
}

...

dictionary.RemoveAll(x => x.Member == foo);
aku
+2  A: 

Instead of removing, just do the inverse. Create a new dictionary from the old one containing only the elements you are interested in.

public Dictionary<T, U> NewDictionaryFiltered<T, U>
(
  Dictionary<T, U> source,
  Func<T, U, bool> filter
)
{
return source
  .Where(x => filter(x.Key, x.Value))
  .ToDictionary(x => x.Key, x => x.Value);
}
David B