Modifica dei valori del dizionario in un ciclo foreach


192

Sto cercando di costruire un grafico a torta da un dizionario. Prima di visualizzare il grafico a torta, desidero riordinare i dati. Sto rimuovendo tutte le sezioni di torta che sarebbero inferiori al 5% della torta e le inserirò in una sezione "Altro". Tuttavia sto ottenendo Collection was modified; enumeration operation may not executeun'eccezione in fase di esecuzione.

Capisco perché non è possibile aggiungere o rimuovere elementi da un dizionario mentre si scorre su di essi. Tuttavia non capisco perché non puoi semplicemente modificare un valore per una chiave esistente all'interno del ciclo foreach.

Eventuali suggerimenti in merito: correzione del mio codice, sarebbero apprezzati.

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

Risposte:


262

L'impostazione di un valore in un dizionario aggiorna il suo "numero di versione" interno, che invalida l'iteratore e qualsiasi iteratore associato alla raccolta di chiavi o valori.

Vedo il tuo punto, ma allo stesso tempo sarebbe strano se la raccolta di valori potesse cambiare a metà iterazione - e per semplicità c'è solo un numero di versione.

Il modo normale di risolvere questo genere di cose è o copiare in anticipo la raccolta di chiavi e iterare sulla copia, oppure scorrere la raccolta originale ma mantenere una raccolta di modifiche che verranno applicate dopo aver terminato l'iterazione.

Per esempio:

Copiare prima le chiavi

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

O...

Creazione di un elenco di modifiche

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
So che questo è vecchio, ma se si utilizza .NET 3.5 (o è 4.0?) È possibile utilizzare e abusare di LINQ come segue: foreach (chiave stringa in colStates.Keys.ToList ()) {...}
Machtyn

6
@Machtyn: Certo - ma la domanda era specificamente .NET 2.0, altrimenti io di certo avrei avere LINQ utilizzato.
Jon Skeet,

Il "numero di versione" fa parte dello stato visibile del Dizionario o di un dettaglio di implementazione?
Coward anonimo il

@SEinfringescopyright: non è visibile direttamente; il fatto che l'aggiornamento del dizionario invalida l'iteratore è comunque visibile.
Jon Skeet,

Da Microsoft Docs .NET framework 4.8 : L'istruzione foreach è un wrapper attorno all'enumeratore, che consente solo la lettura dalla raccolta, non la scrittura su di essa. Quindi direi che si tratta di un dettaglio di implementazione che potrebbe cambiare in una versione futura. E ciò che è visibile è che l'utente dell'Enumeratore ha violato il suo contratto. Ma mi sbaglierei ... è davvero visibile quando un dizionario è serializzato.
Coward anonimo il

81

Chiama ToList()il foreachciclo. In questo modo non abbiamo bisogno di una copia della variabile temp. Dipende da Linq che è disponibile da .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;
    }
}

Miglioramento molto bello!
SpeziFish,

1
Sarebbe meglio usare foreach(var pair in colStates.ToList())per evitare di avere accesso alla chiave e al valore che evita di dover chiamare in colStates[key].
user2864740

21

Stai modificando la raccolta in questa riga:

colStates [chiave] = 0;

In questo modo, essenzialmente stai eliminando e reinserendo qualcosa a quel punto (per quanto riguarda IEnumerable comunque.

Se modifichi un membro del valore che stai memorizzando, sarebbe OK, ma stai modificando il valore stesso e IEnumberable non ti piace.

La soluzione che ho usato è eliminare il ciclo foreach e usare solo un ciclo for. Un semplice ciclo per non controlla le modifiche che sai non influenzeranno la raccolta.

Ecco come puoi farlo:

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

Ottengo questo problema usando for loop. dizionario [indice] [chiave] = "abc", ma torna al valore iniziale "xyz"
Nick Chan Abdullah,

1
La correzione in questo codice non è il ciclo for: sta copiando l'elenco di chiavi. (Funzionerebbe comunque se lo convertissi in un ciclo foreach.) Risolvere usando un ciclo for significherebbe usare colStates.Keysal posto di keys.
idbrii,

6

Non è possibile modificare le chiavi né i valori direttamente in ForEach, ma è possibile modificarne i membri. Ad esempio, questo dovrebbe funzionare:

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

Che ne dici di fare solo alcune query linq sul tuo dizionario e quindi associare il tuo grafico ai risultati di quelli? ...

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 non è disponibile solo in 3.5? Sto usando .net 2.0.
Aheho,

Puoi usarlo dalla 2.0 con un riferimento alla versione 3.5 di System.Core.DLL - se non è qualcosa che vorresti intraprendere fammelo sapere e cancellerò questa risposta.
Scott Ivey,

1
Probabilmente non seguirò questa strada, ma è comunque un buon suggerimento. Ti suggerisco di lasciare la risposta in atto nel caso in cui qualcun altro con lo stesso problema si imbattesse in essa.
Aheho,

3

Se ti senti creativo, potresti fare qualcosa del genere. Scorri indietro nel dizionario per apportare le modifiche.

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

}

Certamente non identico, ma potresti essere interessato comunque ...


2

È necessario creare un nuovo dizionario dal vecchio anziché modificarlo in atto. Qualcosa del genere (iterare anche su KeyValuePair <,> anziché utilizzare una ricerca chiave:

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

A partire da .NET 4.5 È possibile farlo con 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);

Si noti tuttavia che le sue prestazioni sono in realtà molto peggio di un semplice 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>();
    }
}

Risultato:

              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

Non è possibile modificare la raccolta, nemmeno i valori. È possibile salvare questi casi e rimuoverli in un secondo momento. Sarebbe finito così:

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

È possibile modificare la raccolta. Non è possibile continuare a utilizzare un iteratore in una raccolta modificata.
user2864740

0

Disclaimer: non faccio molto C #

Si sta tentando di modificare l'oggetto DictionaryEntry archiviato nella tabella hash. Hashtable memorizza solo un oggetto: l'istanza di DictionaryEntry. La modifica della chiave o del valore è sufficiente per modificare la tabella hash e rendere non valido l'enumeratore.

Puoi farlo al di fuori del ciclo:

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

creando innanzitutto un elenco di tutte le chiavi dei valori che si desidera modificare e scorrere invece tale elenco.


0

È possibile creare una copia dell'elenco di dict.Values, quindi è possibile utilizzare la List.ForEachfunzione lambda per l'iterazione (o un foreachciclo, come suggerito prima).

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

0

Insieme alle altre risposte, ho pensato che avrei notato che se le ottieni sortedDictionary.Keyso sortedDictionary.Valuespoi le foreachcerchi, anche tu vai in ordine. Questo perché quei metodi restituiscono System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectiono SortedDictionary<TKey,TValue>.ValueCollectionoggetti, che mantengono il tipo di dizionario originale.


0

Questa risposta è per confrontare due soluzioni, non una soluzione suggerita.

Invece di creare un altro elenco come suggerito da altre risposte, è possibile utilizzare un forciclo utilizzando il dizionario Countper la condizione di arresto del ciclo e Keys.ElementAt(i)per ottenere la chiave.

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

All'inizio ho pensato che sarebbe stato più efficiente perché non abbiamo bisogno di creare un elenco di chiavi. Dopo aver eseguito un test ho scoperto che la forsoluzione loop è molto meno efficiente. Il motivo è perché ElementAtè O (n) sulla dictionary.Keysproprietà, cerca dall'inizio della raccolta fino a raggiungere l'ennesimo elemento.

Test:

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

risultati:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.