在foreach循环中编辑字典值

241

我正在尝试从一个字典中构建饼图。在显示饼图之前,我希望清理数据。我正在删除任何小于饼图总量5%的扇区,并将它们放入“其他”扇区。但是运行时我遇到了Collection was modified; enumeration operation may not execute异常。

我理解为什么不能在迭代过程中添加或删除字典中的项。但是我不明白为什么不能在foreach循环中仅更改现有键的值。

欢迎任何关于修复我的代码的建议。

Dictionary<string, int> colStates = new Dictionary<string,int>();
// ...
// Some code to populate colStates dictionary
// ...

int OtherCount = 0;

foreach(string key in colStates.Keys)
{

    double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

colStates.Add("Other", OtherCount);

1
在.NET5中,您可以这样做。 - tymtam
14个回答

302

在字典中设置一个值会更新其内部的“版本号”——这将使迭代器和与键或值集合相关联的任何迭代器无效。

我理解您的观点,但同时,如果在迭代过程中值集合发生变化会很奇怪,出于简单起见,只有一个版本号。

修复此类问题的常规方法是要么事先复制键的集合并对其进行迭代,要么对原始集合进行迭代,但维护一组更改,待迭代完成后再应用这些更改。

例如:

首先复制键

List<string> keys = new List<string>(colStates.Keys);
foreach(string key in keys)
{
    double percent = colStates[key] / TotalCount;    
    if (percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

或者...

创建修改列表

List<string> keysToNuke = new List<string>();
foreach(string key in colStates.Keys)
{
    double percent = colStates[key] / TotalCount;    
    if (percent < 0.05)
    {
        OtherCount += colStates[key];
        keysToNuke.Add(key);
    }
}
foreach (string key in keysToNuke)
{
    colStates[key] = 0;
}

34
我知道这个很老了,但如果使用.NET 3.5(或者是4.0?),你可以按照以下方式使用和滥用LINQ:foreach(string key in colStates.Keys.ToList()) {...} - Machtyn
7
@Machtyn:当然可以,但问题特别指明是.NET 2.0,否则我肯定会使用LINQ。 - Jon Skeet
1
@SEinfringescopyright:它并不是直接可见的;然而,更新字典会使迭代器失效这一事实是明显可见的。 - Jon Skeet
2
显然,在 .net5 中,当迭代时使用 setter 更新字典值是被允许的。 - vc 74
1
@JonSkeet,天哪,不要你的答案!这是MSFT没有做好清理工作。我会用非常不同的语言回应任何人。对于几乎是“最大和最古老的软件公司”,我们应该期望比我发现的更好。我从Win NT&C ++转到Linux&Java,然后在2021年为了改变一些节奏转到了C#,但很遗憾,这是一个失望。 - Tjunkie
显示剩余9条评论

119

foreach循环中调用ToList()。这样我们就不需要一个临时变量副本。这取决于Linq,它自.NET 3.5以来可用。

using System.Linq;

foreach(string key in colStates.Keys.ToList())
{
  double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

1
非常好的改进! - SpeziFish
6
最好使用foreach(var pair in colStates.ToList())来避免同时访问键和值,这样就不需要调用colStates[key] - user2864740

23

这一行代码会修改集合:

colStates[key] = 0;

这样做实际上相当于在该位置删除并重新插入某些东西(就IEnumerable而言是这样的)。

如果您编辑所存储值的成员,那么是可以的,但是您正在编辑值本身,而IEnumberable不喜欢那样。

我使用的解决方案是消除foreach循环,只使用for循环。一个简单的for循环不会检查已知不影响集合的更改。

以下是示例代码:

List<string> keys = new List<string>(colStates.Keys);
for(int i = 0; i < keys.Count; i++)
{
    string key = keys[i];
    double  Percent = colStates[key] / TotalCount;
    if (Percent < 0.05)    
    {        
        OtherCount += colStates[key];
        colStates[key] = 0;    
    }
}

我使用for循环时遇到了这个问题。dictionary[index][key] = "abc",但它会恢复到初始值"xyz"。 - Nick Chan Abdullah
3
这段代码的问题不在于for循环,而是复制键列表。(如果将其转换为foreach循环,它仍然可以工作。)使用for循环解决问题意味着在keys的位置使用colStates.Keys - idbrii

6

在ForEach循环中,您无法直接修改键或值,但可以修改它们的成员。例如,以下代码应该可以正常工作:

public class State {
    public int Value;
}

...

Dictionary<string, State> colStates = new Dictionary<string,State>();

int OtherCount = 0;
foreach(string key in colStates.Keys)
{
    double  Percent = colStates[key].Value / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key].Value;
        colStates[key].Value = 0;
    }
}

colStates.Add("Other", new State { Value =  OtherCount } );

6

3

你可以对字典进行一些Linq查询,然后将图表绑定到这些结果上,如何?...

var under = colStates.Where(c => (decimal)c.Value / (decimal)totalCount < .05M);
var over = colStates.Where(c => (decimal)c.Value / (decimal)totalCount >= .05M);
var newColStates = over.Union(new Dictionary<string, int>() { { "Other", under.Sum(c => c.Value) } });

foreach (var item in newColStates)
{
    Console.WriteLine("{0}:{1}", item.Key, item.Value);
}

LINQ不仅在3.5中可用吗?我正在使用.NET 2.0。 - Aheho
你可以在2.0中使用对System.Core.DLL 3.5版本的引用 - 如果这不是你想要尝试的,请告诉我,我会删除这个答案。 - Scott Ivey
1
我可能不会选择这条路,但无论如何这是一个好的建议。 我建议您保留答案,以防其他遇到相同问题的人偶然发现它。 - Aheho

3
如果你有创意,可以这样做。通过反向循环字典来进行更改。
Dictionary<string, int> collection = new Dictionary<string, int>();
collection.Add("value1", 9);
collection.Add("value2", 7);
collection.Add("value3", 5);
collection.Add("value4", 3);
collection.Add("value5", 1);

for (int i = collection.Keys.Count; i-- > 0; ) {
    if (collection.Values.ElementAt(i) < 5) {
        collection.Remove(collection.Keys.ElementAt(i)); ;
    }

}

当然不是完全相同,但你可能仍然会感兴趣...

2
您需要从旧的字典中创建一个新的字典,而不是直接修改原有字典。可以尝试使用类似以下方式创建新字典(同时遍历KeyValuePair<,>而不是使用键查找):
int otherCount = 0;
int totalCounts = colStates.Values.Sum();
var newDict = new Dictionary<string,int>();
foreach (var kv in colStates) {
  if (kv.Value/(double)totalCounts < 0.05) {
    otherCount += kv.Value;
  } else {
    newDict.Add(kv.Key, kv.Value);
  }
}
if (otherCount > 0) {
  newDict.Add("Other", otherCount);
}

colStates = newDict;

2

从.NET 4.5开始,您可以使用ConcurrentDictionary来实现此操作:

using System.Collections.Concurrent;

var colStates = new ConcurrentDictionary<string,int>();
colStates["foo"] = 1;
colStates["bar"] = 2;
colStates["baz"] = 3;

int OtherCount = 0;
int TotalCount = 100;

foreach(string key in colStates.Keys)
{
    double Percent = (double)colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

colStates.TryAdd("Other", OtherCount);

请注意,实际上它的性能比简单的 foreach dictionary.Kes.ToArray() 要差得多:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class ConcurrentVsRegularDictionary
{
    private readonly Random _rand;
    private const int Count = 1_000;

    public ConcurrentVsRegularDictionary()
    {
        _rand = new Random();
    }

    [Benchmark]
    public void ConcurrentDictionary()
    {
        var dict = new ConcurrentDictionary<int, int>();
        Populate(dict);

        foreach (var key in dict.Keys)
        {
            dict[key] = _rand.Next();
        }
    }

    [Benchmark]
    public void Dictionary()
    {
        var dict = new Dictionary<int, int>();
        Populate(dict);

        foreach (var key in dict.Keys.ToArray())
        {
            dict[key] = _rand.Next();
        }
    }

    private void Populate(IDictionary<int, int> dictionary)
    {
        for (int i = 0; i < Count; i++)
        {
            dictionary[i] = 0;
        }
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<ConcurrentVsRegularDictionary>();
    }
}

结果:

              Method |      Mean |     Error |    StdDev |
--------------------- |----------:|----------:|----------:|
 ConcurrentDictionary | 182.24 us | 3.1507 us | 2.7930 us |
           Dictionary |  47.01 us | 0.4824 us | 0.4512 us |

1

你无法修改集合,甚至不允许修改其中的值。你可以保存这些案例并稍后删除它们。最终会变成这样:

Dictionary<string, int> colStates = new Dictionary<string, int>();
// ...
// Some code to populate colStates dictionary
// ...

int OtherCount = 0;
List<string> notRelevantKeys = new List<string>();

foreach (string key in colStates.Keys)
{

    double Percent = colStates[key] / colStates.Count;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        notRelevantKeys.Add(key);
    }
}

foreach (string key in notRelevantKeys)
{
    colStates[key] = 0;
}

colStates.Add("Other", OtherCount);

可以修改集合,但是不能在修改后继续使用一个迭代器。 - user2864740

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接