我正试图从字典中建立一个饼图。在显示饼图之前,我想整理一下数据。我去掉了所有小于5%的派片,把它们放到“其他”派片里。然而,我得到一个集合被修改;枚举操作在运行时不能执行异常。

我理解为什么在遍历字典时不能从字典中添加或删除项。但是,我不明白为什么不能简单地在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);

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

我明白你的观点,但同时,如果值集合可以在迭代过程中改变,那就太奇怪了——而且为了简单起见,只有一个版本号。

修复这类问题的正常方法是预先复制键的集合并遍历副本,或者遍历原始集合,但保留在完成迭代后应用的更改集合。

例如:

先复制键

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

您需要从旧的Dictionary中创建一个新的Dictionary,而不是原地修改。有些像(也迭代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;

免责声明:我不怎么使用c#

您正在尝试修改存储在哈希表中的DictionaryEntry对象。哈希表只存储一个对象——DictionaryEntry的实例。改变键或值就足以改变哈希表并导致枚举数失效。

你可以在循环之外执行:

if(hashtable.Contains(key))
{
    hashtable[key] = value;
}

首先,创建一个包含您希望更改的值的所有键的列表,然后遍历该列表。


您不能修改集合,甚至不能修改值。您可以保存这些案例,并在以后删除它们。结果是这样的:

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

你在这一行修改集合:

col各州[键]= 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;    
    }
}

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

如何只是对你的字典做一些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);
}

如果你觉得有创意,你可以这样做。在字典中进行反向循环以进行更改。

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

}

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


在foreach循环中调用ToList()。这样我们就不需要一个临时变量的拷贝。它依赖于自。net 3.5以来可用的Linq。

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

你可以把字典做一个列表副本。值,然后您可以使用列表。ForEach lambda函数用于迭代(或如前所述的ForEach循环)。

new List<string>(myDict.Values).ForEach(str =>
{
  //Use str in any other way you need here.
  Console.WriteLine(str);
});

从。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差得多。

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 |

与其他答案一起,我想我应该注意一下,如果您获得sortedDictionary。Keys或sortedDictionary。值,然后用foreach遍历它们,你也会按顺序遍历。这是因为这些方法返回System.Collections.Generic.SortedDictionary<TKey,TValue>。KeyCollection或SortedDictionary<TKey,TValue>。ValueCollection对象,它维护原始字典的排序。


这个答案用于比较两个解决方案,而不是建议的解决方案。

您可以使用for循环,使用字典Count作为循环停止条件,并使用Keys.ElementAt(i)获取键,而不是像其他答案建议的那样创建另一个列表。

for (int i = 0; i < dictionary.Count; i++)
{
    dictionary[dictionary.Keys.ElementAt(i)] = 0;
}

起初,我认为这将是更有效的,因为我们不需要创建一个键列表。在运行测试后,我发现for循环解决方案的效率要低得多。原因是ElementAt在字典上是O(n)。属性时,它从集合的开始搜索,直到找到第n个项。

测试:

int iterations = 10;
int dictionarySize = 10000;
Stopwatch sw = new Stopwatch();

Console.WriteLine("Creating dictionary...");
Dictionary<string, int> dictionary = new Dictionary<string, int>(dictionarySize);
for (int i = 0; i < dictionarySize; i++)
{
    dictionary.Add(i.ToString(), i);
}
Console.WriteLine("Done");

Console.WriteLine("Starting tests...");

// for loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    for (int j = 0; j < dictionary.Count; j++)
    {
        dictionary[dictionary.Keys.ElementAt(j)] = 3;
    }
}
sw.Stop();
Console.WriteLine($"for loop Test:     {sw.ElapsedMilliseconds} ms");

// foreach loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    foreach (string key in dictionary.Keys.ToList())
    {
        dictionary[key] = 3;
    }
}
sw.Stop();
Console.WriteLine($"foreach loop Test: {sw.ElapsedMilliseconds} ms");

Console.WriteLine("Done");

结果:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done

在。net 5中,可以在枚举字典时更改字典项。

拉请求是:允许在枚举期间覆盖字典,问题是考虑从字典<TKey, TValue>的覆盖中删除_version++。

现在你可以:

foreach (var pair in dict)
    dict[pair.Key] = pair.Value + 1;