Редагування значень словника в циклі foreach


192

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

Відповіді:


262

Встановлення значення в словнику оновлює його внутрішній "номер версії" - що приводить до недійсності ітератора та будь-якого ітератора, пов'язаного зі збиранням ключів або значень.

Я бачу вашу думку, але в той же час було б дивним, якби збір значень міг змінити середину ітерації - а для простоти існує лише один номер версії.

Нормальний спосіб виправити подібний предмет - це попередньо скопіювати колекцію ключів і повторити її над копією, або повторити її над оригінальною колекцією, але зберегти колекцію змін, які ви застосуєте після завершення ітерації.

Наприклад:

Копіювання ключів спочатку

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

24
Я знаю, що це давнє, але якщо ви користуєтесь .NET 3.5 (або це 4.0?), Ви можете використовувати та зловживати LINQ таким чином: foreach (рядовий ключ у colStates.Keys.ToList ()) {...}
Machtyn

6
@Machtyn: Звичайно, але питання стосувалося конкретно .NET 2.0, інакше я б точно використовував LINQ.
Джон Скіт

Чи "номер версії" є частиною видимого стану Словника або деталі реалізації?
Анонімний боягуз

@SEinfringescopyright: це не видно безпосередньо; той факт , що оновлення словника Анулює итератор є видимим , хоча.
Джон Скіт

З Microsoft Docs .NET Framework 4.8 : Заява foreach - це обгортка навколо нумератора, яка дозволяє лише читати з колекції, а не писати на ній. Тому я б сказав, що це деталізація щодо впровадження, яка може змінитись у майбутній версії. І що видно, це те, що користувач Переписувача порушив свій договір. Але я помиляюся ... це дійсно видно, коли Словник серіалізується.
Анонімний боягуз

81

Виклик 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;
    }
}

Дуже приємне поліпшення!
SpeziFish

1
Було б краще використовувати, foreach(var pair in colStates.ToList())щоб не мати доступу до ключа та значення, що дозволяє уникнути colStates[key]
входу

21

Ви змінюєте колекцію в цьому рядку:

colStates [ключ] = 0;

Роблячи це, ви по суті видаляєте і повторно вставляєте щось у цьому пункті (наскільки це стосується IEnumerable у будь-якому випадку).

Якщо ви редагуєте учасника значення, яке ви зберігаєте, це було б добре, але ви редагуєте саме значення, і IEnumberable це не любить.

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

Ось як ви могли це зробити:

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

Я отримую цю проблему за допомогою циклу. словник [індекс] [key] = "abc", але він повертається до початкового значення "xyz"
Нік Чан Абдулла

1
Виправлення в цьому коді не для циклу: це копіювання списку ключів. (Це все одно буде працювати, якщо ви перетворили його в цикл foreach.) Розв’язання за допомогою циклу for означатиме використання colStates.Keysзамість keys.
idbrii

6

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

3

Як щодо того, щоб просто виконати кілька запитів 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.
Aheho

Ви можете використовувати його з 2.0 із посиланням на 3.5 версію System.Core.DLL - якщо це не те, що ви хочете взяти на себе, дайте мені знати, і я видалю цю відповідь.
Скотт Айві

1
Я, мабуть, не збираюся йти цим маршрутом, але все-таки гарна пропозиція. Я пропоную вам залишити відповідь на місці, якщо хтось інший із тим самим питанням наткнеться на неї.
Ахехо

3

Якщо ви відчуваєте себе креативними, можете зробити щось подібне. Прокрутіть назад через словник, щоб внести зміни.

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

}

Звичайно, не тотожна, але вас можуть зацікавити будь-коли ...


2

Вам потрібно створити новий Словник зі старого, а не змінювати його на місці. Щось подібне (також повторіть 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;

1

Починаючи з .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 |

1

Ви не можете змінювати колекцію, навіть значення. Ви можете зберегти ці випадки та видалити їх пізніше. Це закінчиться так:

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

Ви можете змінити колекцію. Ви не можете продовжувати використовувати ітератор для модифікованої колекції.
користувач2864740

0

Відмова від відповідальності: я не дуже багато C #

Ви намагаєтесь змінити об'єкт DictionaryEntry, який зберігається в HashTable. Hashtable зберігає лише один об'єкт - ваш примірник DictionaryEntry. Змінення ключа або значення достатньо, щоб змінити HashTable і призвести до того, що нумератор стане недійсним.

Ви можете зробити це поза циклу:

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

спочатку створивши список усіх клавіш значень, які ви хочете змінити, і замість цього повторіть список.


0

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

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

0

Поряд з іншими відповідями, я подумав, що зауважу, що якщо ви отримаєте sortedDictionary.Keysабо sortedDictionary.Valuesпотім переведіть їх на цикл foreach, ви також пройдете в упорядкованому порядку. Це тому, що ті методи повертають System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionабо SortedDictionary<TKey,TValue>.ValueCollectionоб'єкти, які підтримують різновид початкового словника.


0

Ця відповідь призначена для порівняння двох рішень, а не запропонованого рішення.

Замість створення іншого списку, як запропоновано інші відповіді, ви можете використовувати forцикл, використовуючи словник Countдля умови зупинки циклу та Keys.ElementAt(i)отримати ключ.

for (int i = 0; i < dictionary.Count; i++)
{
    dictionary[dictionary.Keys.ElementAt(i)] = 0;
}

На ялинках я думав, що це буде більш ефективно, оскільки нам не потрібно створювати список ключів. Після запуску тесту я виявив, що forциклічне рішення набагато менш ефективне. Причина полягає в тому ElementAt, що O (n) у dictionary.Keysвластивості він здійснює пошук з початку колекції до тих пір, поки не потрапить до 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.