关于C#:在foreach循环中编辑字典值

Editing dictionary values in a foreach loop

我试着从词典中构建一个喜鹊图。在我展示喜鹊图之前,我想把数据输入。我把每一块碎屑都清理干净了,它们的碎屑比碎屑的5%还要少,并将它们放在一个"其他"的碎屑中。但我还是得到了一个Collection was modified; enumeration operation may not execute例外。

我明白你为什么不能从词典中添加或删除项目,而在词典中重复这些项目。但我不明白为什么你不能简单地改变一个在前轮中存在的关键。

有什么建议:确定我的代码,值得赞赏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
2
3
4
5
6
7
8
9
10
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;
    }
}

或者…

创建修改列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}


调用foreach循环中的ToList()。这样我们就不需要临时变量副本。它取决于Linq,它是从.NET 3.5开始提供的。

1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
}


您正在修改此行中的集合:

colStates[key] = 0;

通过这样做,您基本上就是在那一点上删除和重新插入某些内容(就IEnumerable而言,无论如何都是这样)。

如果编辑要存储的值的成员,这没关系,但您正在编辑值本身,IEnumerable不喜欢这样。

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

你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
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中修改键或值,但可以修改它们的成员。例如,这应该有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 } );

如果你觉得有创造力,你可以这样做。向后循环浏览字典以进行更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
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)); ;
    }

}

当然不完全相同,但你可能对…


只需对字典执行一些LINQ查询,然后将图形绑定到这些查询的结果中,如何?…

1
2
3
4
5
6
7
8
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);
}


您需要从旧词典创建一个新词典,而不是就地修改。一些类似的(也迭代keyValuePair<,>而不是使用键查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        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);


除了其他答案,我想我会注意到,如果你得到sortedDictionary.KeyssortedDictionary.Values,然后用foreach循环它们,你也会按顺序进行。这是因为这些方法返回System.Collections.Generic.SortedDictionary.KeyCollectionSortedDictionary.ValueCollection对象,这些对象维护原始字典的类型。


从.NET 4.5开始,您可以使用ConcurrentDictionary执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()差得多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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>();
    }
}

结果:

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

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

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


免责声明:我不做太多C#

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

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

1
2
3
4
if(hashtable.Contains(key))
{
    hashtable[key] = value;
}

首先创建一个列表,列出您希望更改的值的所有键,然后迭代该列表。