Редактирование значений словаря в цикле foreach



Я пытаюсь построить круговую диаграмму из словаря. Прежде чем я покажу круговую диаграмму, я хочу привести в порядок данные. Я удаляю любые куски пирога, которые были бы менее 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);
726   12  

12 ответов:

установка значения в словаре обновляет его внутренний "номер версии", что делает недействительным итератор и любой итератор, связанный с коллекцией ключей или значений.

я поняла твою точку зрения, но в то же время было бы странно, если набор значений может меняться в середине итерации и для простоты есть только один номер версии.

обычный способ исправления такого рода вещей заключается в том, чтобы либо скопировать коллекцию ключей заранее и повторить копию, или повторите исходную коллекцию, но сохраните коллекцию изменений, которые вы примените после завершения итерации.

например:

копирование ключей первого

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

вызов ToList() на foreach петли. Таким образом, нам не нужна временная переменная копия. Это зависит от Linq, который доступен с .Net 3.5.

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 в любом случае.

Если вы редактируете a из значения, которое вы храните, это было бы нормально, но вы редактируете само значение, и 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;    
    }
}

вы не можете изменить ключи или значения непосредственно в 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 } );

Как насчет того, чтобы просто сделать некоторые запросы 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);
}

Если вы чувствуете себя творческим, вы могли бы сделать что-то подобное. Петли назад через словарь, чтобы внести изменения.

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

}

конечно, не идентичны, но вы могли бы быть заинтересованы в любом случае...

вам нужно создать новый словарь из старого, а не изменять на месте. Что-то вроде (также перебирайте 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;

вы не можете изменить коллекцию, даже не ценности. Вы можете сохранить эти случаи и удалить их позже. Это закончилось бы так:

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

отказ от ответственности: я не делаю много C#

вы пытаетесь изменить объект DictionaryEntry, который хранится в хэш-таблице. В хэш-таблице хранится только один объект-ваш экземпляр DictionaryEntry. Изменение ключа или значения достаточно, чтобы изменить хэш-таблицу и привести к тому, что перечислитель станет недействительным.

вы можете сделать это вне цикла:

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

сначала создав список всех ключей значений, которые вы хотите изменить и повторить вместо этого список.

вы можете сделать копию списка dict.Values, вы можете использовать List.ForEach лямбда-функцию для итерации (или foreach цикл, как предлагалось ранее).

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

начиная с .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 |

наряду с другими ответами, я думал, что отмечу, что если вы получите sortedDictionary.Keys или sortedDictionary.Values а затем цикл над ними с foreach, вы также проходите в отсортированном порядке. Это потому, что эти методы возвращают System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollection или SortedDictionary<TKey,TValue>.ValueCollection объекты, которые поддерживают вид исходного словаря.

Comments

    Ничего не найдено.