【问题标题】:Editing dictionary values in a foreach loop在 foreach 循环中编辑字典值
【发布时间】: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);

【问题讨论】:

标签: c# .net .net-2.0


【解决方案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
                  

                  【讨论】:

                    猜你喜欢
                    • 2015-07-25
                    • 2013-12-24
                    • 2010-12-01
                    • 1970-01-01
                    • 2012-03-11
                    • 2019-05-02
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多