题 从c#Dictionary中删除与谓词匹配的多个项目的最佳方法?


我需要从Dictionary中删除多个项目。 一种简单的方法如下:

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

我不能直接删除foreach块中的项目的原因是这会抛出异常(“Collection was modified ...”)

我想做以下事情:

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

但是Dictionary <>类没有公开RemoveAll(Predicate <> Match)方法,就像List <> Class那样。

这样做的最佳方式(性能明智和优雅明智)是什么?


49
2018-01-22 13:57


起源




答案:


这是另一种方式

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

直接将代码推送到列表中可以避免“在枚举时删除”问题。该 .ToList() 将在foreach真正开始之前强制枚举。


78
2018-01-22 14:06



很好的答案,但我不认为ToList()是必需的。是吗? - Jim Mischel
很好的答案,但如果我没有错,s是Value类型的一个实例,那么结束的s.key将无法编译,或者它会不会? - Brann
由于“.ToList()”,我认为我不太喜欢这个解决方案。它有一个目的,但它的目的是不明确的,直到你删除.ToList()并自己观察错误。当有更多可读的替代方案时,我不建议使用此代码。 - Juliet
@JaredPar,请修复你的答案,很有趣的是看到大多数投票的答案都不会编译。此外,没有必要查询值,您可以直接获取密钥 - aku
@Jim,这里绝对需要.ToList。 foreach主体修改底层集合。如果没有.ToList(),Where子句将针对修改后的集合进行操作。使用.ToList()强制查询在发生任何删除之前完成 - JaredPar


你可以创建一个 扩展方法

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

...

dictionary.RemoveAll(x => x.Member == foo);

19
2018-01-22 14:15





而不是删除,只是做反过来。从旧的字典创建一个只包含您感兴趣的元素的字典。

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

10
2018-01-22 15:08



过滤器来自哪里?你能解释一下它是什么吗? - Lyubomir Velchev
您可能需要添加 using System.Linq; 使这项工作。 - Gerard


修改版Aku的扩展方法解决方案。主要区别在于它允许谓词使用字典键。一个细微的差别是它扩展了IDictionary而不是Dictionary。

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

. . .

dictionary.RemoveAll((k,v) => v.Member == foo);

10
2017-08-22 13:44



我推回了社区的变更,因为它没有包含 ToList() 导致“在枚举时删除”问题。 - wimh
感谢杰罗姆的灵感。除了这个和@Aku的答案,我创建了扩展重载 Func<TKey, bool> 和 Func<KeyValuePair<TKey, TValue>> 所有这些都是相互配合的(除非是 TKey 和 TValue 当编译器显然无法在两者之间进行选择时属于同一类型 Func<TKey, bool>或者 Func<TValue, bool>)。如果有兴趣的人无法弄清楚如何实现它们,请在这里ping我,我会发布它们。 :-) - Ulf Åkerstedt


你能改变循环使用索引(即FOR而不是FOREACH)吗?当然,你必须向后循环,即将count-1降为零。


0
2018-01-22 14:06



您无法使用FOR迭代字典。 - Brann
对不起,我认为扩展名.ElementAt(index)会允许这样做,至少在.net 3.5中。 - Geoff
@brann哦,你可以使用linq。 for(int index = 0; index <MyCollection.Count; index ++){var kvp = MyCollection.ElementAt(index); var kvp = item.Key; var kvp = item.Value; } - Offler
@Geoff:假设字典包含三个键:“Moe”,“Larry”和“Curly”。 };一个人想要删除所有不以“C”开头的键。第一次打电话给 ElementAt(2) 将枚举所有三个项目并返回Curly,不应删除。然后 ElementAt(1) 将列举两个项目,并返回拉里。删除拉里可能会随意重新排序项目,所以 ElementAt(0) 可能会回到Moe或Curly。如果碰巧返回Curly,那么Moe最终不会被处理。 ElementAt 可能是合法的,但这并不意味着它会有效。 - supercat


而不是只删除反向(从只包含您感兴趣的元素的旧字典创建一个新字典),让垃圾收集器处理旧字典:

var newDictionary = oldDictionary.Where(x => x.Value != foo);

-1
2018-01-22 14:07



这可能会导致糟糕的表现,不是吗? - Brann
使用var关键字,这不会给newDictionary提供一种IEnumerable <KeyValuePair <TKey,TValue >>,而不是Dictionary <TKey,TValue>? - Chris Ammerman
Enumerable.Select不做过滤。 - Amy B