2009-07-01 13 views
148

Sto provando a creare un grafico a torta da un dizionario. Prima di visualizzare il grafico a torta, voglio riordinare i dati. Sto rimuovendo qualsiasi porzione di torta che sarebbe inferiore al 5% della torta e inserendoli in una fetta di torta "Altro". Tuttavia ricevo un'eccezione Collection was modified; enumeration operation may not execute in fase di runtime.Modifica dei valori del dizionario in un ciclo foreach

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

Eventuali suggerimenti per: risolvere il 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); 

risposta

204

impostazione di un valore in un dizionario aggiorna il suo "numero di versione" interna - che invalida l'iteratore, e qualsiasi iteratore associato con le chiavi o valori di raccolta.

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

Il modo normale di risolvere questo tipo di cose è copiare la raccolta di chiavi in ​​anticipo e scorrere sulla copia, o scorrere sulla raccolta originale mantenendo una serie di modifiche che verranno applicate dopo aver terminato iterazione.

Ad esempio:

chiavi Copia primi

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

So che questo è vecchio, ma se si utilizza .NET 3.5 (o è 4.0?) È possibile utilizzare e abusare LINQ come segue: foreach (stringa chiave in colStates.Keys.ToList()) {...} – Machtyn

+3

@Machtyn: Certo - ma la domanda riguardava specificamente .NET 2.0, altrimenti avrei certamente * usato * LINQ. –

1

È necessario creat e un nuovo dizionario dal vecchio piuttosto che modificare sul posto. Somethine come (iterare anche sopra il KeyValuePair <,> invece di usare 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; 
0

Disclaimer: Non faccio molto C#

Si sta cercando di modificare l'oggetto DictionaryEntry che è memorizzato in HashTable: Hashtable memorizza solo un oggetto, la tua istanza di DictionaryEntry, modificando la chiave o il valore è sufficiente per modificare l'hashtable e causare l'annullamento dell'enumerazione

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 in quella lista.

1

Non è possibile modificare la raccolta, nemmeno i valori. Potresti salvare questi casi e rimuoverli in seguito.Si finirebbe 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); 
+0

È * possibile * modificare la collezione. Non è possibile * continuare a utilizzare un iteratore per una raccolta modificata. – user2864740

17

Si sta modificando la collezione in questa linea:

colStates [tasto] = 0;

In questo modo, si sono essenzialmente l'eliminazione e reinserire qualcosa in quel punto (per quanto IEnumerable è interessato in ogni modo.

Se si modifica un membro del valore si sta archiviando, che sarebbe OK, ma stai modificando il valore stesso e IEnumberable non gli piace

La soluzione che ho usato è eliminare il ciclo foreach e utilizzare solo un ciclo for Un ciclo for semplice non controlla le modifiche che conosci non influiscono sulla raccolta.

Ecco come si potesse fare:

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

Ho riscontrato questo problema usando per ciclo. dizionario [indice] [chiave] = "abc", ma ritorna al valore iniziale "xyz" –

3

Non è possibile modificare le chiavi né i valori direttamente in un ForEach, ma è possibile modificare i loro 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 dite di fare solo alcune query LINQ contro il dizionario, e quindi vincolante il grafico con i risultati di coloro ...

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

Linq non è disponibile solo in 3.5? Sto usando .net 2.0. – Aheho

+0

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. –

+1

Probabilmente non seguirò questa strada, ma è comunque un buon suggerimento. Ti suggerisco di lasciare la risposta sul posto nel caso in cui qualcun altro con lo stesso problema inciampi. – Aheho

2

Se ti senti? creativo potresti fare qualcosa di simile. Fai scorrere all'indietro il 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 potreste essere interessati in ogni modo ...

44

chiamata la ToList() nel ciclo foreach. In questo modo non abbiamo bisogno di una copia variabile temporanea. Dipende da Linq, 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; 
    } 
} 
+0

Ottimo miglioramento! – SpeziFish

+0

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

0

È possibile effettuare una copia lista del dict.Values, quindi è possibile utilizzare la funzione List.ForEach lambda per l'iterazione, (o un ciclo foreach, come suggerito in precedenza).

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

Si consideri' foreach 'per codice di produzione di effetti collaterali. – user2864740

0

partire con .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); 

Nota tuttavia che le sue prestazioni è in realtà molto peggio che 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 |