在 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.KeyssortedDictionary.Values,然后用foreach 循环它们,你也会按排序顺序进行。这是因为这些方法返回 System.Collections.Generic.SortedDictionary&lt;TKey,TValue&gt;.KeyCollectionSortedDictionary&lt;TKey,TValue&gt;.ValueCollection 对象,它们保持原始字典的排序。

【讨论】:

【参考方案14】:

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

您可以使用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

【讨论】:

以上是关于在 foreach 循环中编辑字典值的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 的 foreach 循环中设置切换状态

字典中的 foreach 解构

为啥在foreach循环中不能修改值类型实例

淘汰赛 JS 选择初始值在 foreach 循环内未正确显示

为啥这个 foreach 循环缺少类中的属性?

为啥我的 forEach 循环没有编辑我的数组? [复制]