在foreach循环中编辑字典值


191

我正在尝试从字典构建饼图。在显示饼图之前,我想整理数据。我要删除所有小于饼图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);

Answers:


260

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

我确实明白您的意思,但是同时,如果values集合可以更改中间迭代,那将很奇怪-为简单起见,只有一个版本号。

解决此类问题的通常方法是预先复制键的集合并在副本上进行迭代,或者在原始集合上进行迭代,但是保留一个更改集合,这些更改将在完成迭代后应用。

例如:

首先复制密钥

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

24
我知道这很旧,但是如果使用.NET 3.5(或者它是4.0?),则可以按以下方式使用和滥用LINQ:foreach(colStates.Keys.ToList()中的字符串键){...}
Machtyn

6
@Machtyn:可以,但是问题特别是关于.NET 2.0,否则我肯定使用LINQ。
乔恩·斯基特

“版本号”是词典可见状态的一部分还是实现细节?
匿名Co夫

@SEinfringescopyright:它不直接可见;但是,更新字典使迭代器无效的事实可见的。
Jon Skeet

来自Microsoft Docs .NET Framework 4.8foreach语句是枚举器的包装,它仅允许从集合中读取内容,而不能对其进行写入。所以我想说这是一个实现细节,将来的版本中可能会更改。可见,枚举器的用户违反了其合同。但是我错了……当字典序列化时,它确实可见。
匿名Co夫

81

ToList()foreach循环中调用。这样,我们就不需要临时变量副本。它取决于从.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;
    }
}

非常好的改进!
SpeziFish 2014年

1
最好使用它foreach(var pair in colStates.ToList())来避免访问键值,从而避免调用colStates[key]..
user2864740

21

您正在以下行中修改集合:

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

1
这段代码中的解决方法不是for循环:它是在复制键列表。(如果将其转换为foreach循环,仍然可以使用。)使用for循环进行求解将意味着使用colStates.Keys代替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 } );

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

您需要从旧版本创建一个新的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;

1

从.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

0

免责声明:我没有做太多的C#

您正在尝试修改存储在HashTable中的DictionaryEntry对象。哈希表仅存储一个对象-您的DictionaryEntry实例。更改键或值足以更改HashTable并导致枚举数变为无效。

您可以在循环外执行此操作:

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

首先创建您要更改的值的所有键的列表,然后遍历该列表。


0

您可以制作的列表副本dict.Values,然后可以使用List.ForEachlambda函数进行迭代(或foreach循环,如前所述)。

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

0

与其他答案一起,我想我要指出的是,如果您获得sortedDictionary.KeyssortedDictionary.Values然后用来遍历它们foreach,那么您也将按照排序顺序进行操作。这是因为那些方法返回 System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionSortedDictionary<TKey,TValue>.ValueCollection保持原始字典排序的对象。


0

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

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

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

首先,我认为这样做会更有效,因为我们不需要创建密钥列表。运行测试后,我发现for循环解决方案的效率要低得多。原因是因为ElementAtdictionary.Keys属性上的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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.