在 foreach 循环中编辑字典值
Posted
技术标签:
【中文标题】在 foreach 循环中编辑字典值【英文标题】:Editing dictionary values in a foreach loop 【发布时间】:2009-07-01 19:00:50 【问题描述】:我正在尝试从字典构建饼图。在显示饼图之前,我想整理一下数据。我正在删除任何小于 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);
【问题讨论】:
在 .NET5 中可以。 ***.com/questions/66939923/… 【参考方案1】:在字典中设置值会更新其内部“版本号”——这会使迭代器以及与键或值集合关联的任何迭代器无效。
我确实明白你的意思,但与此同时,如果值集合在迭代过程中发生变化,那就太奇怪了 - 为简单起见,只有一个版本号。
解决此类问题的正常方法是预先复制键集合并迭代副本,或者迭代原始集合但维护一个您将在完成迭代后应用的更改集合.
例如:
先复制密钥
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;
【讨论】:
我知道这是旧的,但如果使用 .NET 3.5(或者是 4.0?),您可以使用和滥用 LINQ,如下所示: foreach(string key in colStates.Keys.ToList()) ... @Macchtyn:当然——但这个问题专门针对 .NET 2.0,否则我肯定会使用 LINQ。 “版本号”是字典可见状态的一部分还是实现细节? @SEinfringescopyright:它不直接可见;更新字典会使迭代器无效的事实 是 可见的。 显然在.net5中允许迭代时使用setter更新字典值【参考方案2】:在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(var pair in colStates.ToList())
来避免访问键和避免调用colStates[key]
的值..【参考方案3】:
您正在修改此行中的集合:
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" 此代码中的修复不是 for 循环:它是复制键列表。 (如果将其转换为 foreach 循环,它仍然可以工作。)使用 for 循环解决意味着使用colStates.Keys
代替 keys
。【参考方案4】:
您不能直接在 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 );
【讨论】:
【参考方案5】:对你的字典做一些 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。 您可以参考 System.Core.DLL 的 3.5 版本从 2.0 开始使用它 - 如果您不想这样做,请告诉我,我将删除此答案。 我可能不会走这条路,但这是一个很好的建议。我建议您保留答案,以防遇到相同问题的其他人偶然发现它。【参考方案6】:如果你觉得有创意,你可以做这样的事情。向后循环浏览字典以进行更改。
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)); ;
当然不一样,但无论如何你可能会感兴趣......
【讨论】:
【参考方案7】:在 .NET 5 中,可以在枚举字典时更改字典项。
拉取请求是:Allow Dictionary overwrites during enumeration,问题是Consider removing _version++ from overwrites in Dictionary<TKey, TValue>。
现在你可以:
foreach (var pair in dict)
dict[pair.Key] = pair.Value + 1;
【讨论】:
【参考方案8】:您需要从旧字典创建新字典,而不是就地修改。类似的东西(也遍历 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;
【讨论】:
【参考方案9】:从 .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 |
【讨论】:
【参考方案10】:您不能修改集合,甚至不能修改值。您可以保存这些案例并在以后删除它们。最终会是这样:
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);
【讨论】:
您可以修改收藏。您不能继续对修改后的集合使用迭代器。【参考方案11】:免责声明:我不会做太多 C#
您正在尝试修改存储在 HashTable 中的 DictionaryEntry 对象。 Hashtable 只存储一个对象——您的 DictionaryEntry 实例。改变Key或者Value就足以改变HashTable,导致枚举器失效。
你可以在循环之外做:
if(hashtable.Contains(key))
hashtable[key] = value;
首先创建一个包含您希望更改的值的所有键的列表,然后遍历该列表。
【讨论】:
【参考方案12】:您可以制作dict.Values
的列表副本,然后可以使用List.ForEach
lambda 函数进行迭代(或foreach
循环,如前所述)。
new List<string>(myDict.Values).ForEach(str =>
//Use str in any other way you need here.
Console.WriteLine(str);
);
【讨论】:
【参考方案13】:除了其他答案,我想我会注意到,如果你得到sortedDictionary.Keys
或sortedDictionary.Values
,然后用foreach
循环它们,你也会按排序顺序进行。这是因为这些方法返回 System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollection
或 SortedDictionary<TKey,TValue>.ValueCollection
对象,它们保持原始字典的排序。
【讨论】:
【参考方案14】:此答案用于比较两个解决方案,而不是建议的解决方案。
您可以使用for
循环,使用字典Count
作为循环停止条件并使用Keys.ElementAt(i)
来获取密钥,而不是创建另一个列表。
for (int i = 0; i < dictionary.Count; i++)
dictionary[dictionary.Keys.ElementAt(i)] = 0;
起初我认为这会更有效,因为我们不需要创建密钥列表。运行测试后,我发现for
循环解决方案的效率要低得多。原因是因为ElementAt
在dictionary.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
【讨论】:
以上是关于在 foreach 循环中编辑字典值的主要内容,如果未能解决你的问题,请参考以下文章
如何在 SwiftUI 的 foreach 循环中设置切换状态