2009-07-01 20 views
148

Estoy intentando crear un gráfico circular de un diccionario. Antes de mostrar el gráfico circular, quiero ordenar los datos. Estoy eliminando los sectores de tarta que serían menos del 5% de la tarta y los coloco en un segmento de tarta "Otro". Sin embargo, recibo una excepción Collection was modified; enumeration operation may not execute en tiempo de ejecución.Edición de valores de diccionario en un bucle foreach

Entiendo por qué no puede agregar o eliminar elementos de un diccionario mientras itera sobre ellos. Sin embargo, no entiendo por qué no puede simplemente cambiar un valor para una clave existente dentro del ciclo foreach.

Cualquier sugerencia re: arreglar mi código, sería apreciada.

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

Respuesta

204

establecimiento de un valor en un diccionario actualiza su "número de versión" interno - que invalida el iterador, y cualquier iterador asociado con las claves o valores de la colección.

Veo su punto, pero al mismo tiempo sería extraño si la colección de valores pudiera cambiar a mitad de iteración, y para simplificar, solo hay un número de versión.

La forma normal de solucionar este tipo de cosas es copiar previamente la colección de claves e iterar sobre la copia, o iterar sobre la colección original, pero mantener una colección de cambios que aplicará después de que haya terminado iterando

Por ejemplo:

Copia de teclas primeros

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

Creación de una lista de modificaciones

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

Sé que esto es antiguo, pero si usa .NET 3.5 (¿o es 4.0?) Puede usar y abusar LINQ de la siguiente manera: foreach (clave de cadena en colStates.Keys.ToList()) {...} – Machtyn

+3

@Machtyn: Claro, pero la pregunta era específicamente sobre .NET 2.0, de lo contrario ciertamente * habría * usado LINQ. –

1

Se necesitan para conformar e un nuevo diccionario de la antigua en lugar de modificar en su lugar. Somethine como (también iterar sobre la KeyValuePair <,> en lugar de utilizar una búsqueda de claves:

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

Negación: no hago mucho C#

Usted está tratando de modificar el objeto DictionaryEntry que se almacena en la HashTable. La Hashtable solo almacena un objeto: su instancia de DictionaryEntry. Cambiar la clave o el valor es suficiente para cambiar la HashTable y hacer que el enumerador se vuelva inválido.

Puede hacerlo fuera del ciclo:

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

creando primero una lista de todas las claves de los valores que desea cambiar e iterar a través de esa lista.

1

No puede modificar la colección, ni siquiera los valores. Puede guardar estos casos y eliminarlos más tarde.Que terminaría de esta manera:

 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

Usted * puede * modificar la colección. * No puedes * seguir usando un iterador en una colección modificada. – user2864740

17

Usted está modificando la colección en esta línea:

colStates [clave] = 0;

Al hacerlo, va a eliminar en esencia y volver a insertar algo en ese momento (por lo que IEnumerable se ocupa de todos modos.

Si edita un miembro del valor que está almacenando, que sería OK, pero está editando el valor en sí mismo y a IEnumberable no le gusta eso.

La solución que he usado es eliminar el bucle foreach y simplemente usar un ciclo for. Un bucle simple no verificará cambios que usted sabe que no afectarán a la colección.

Así es como podría hacerlo:

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

Obtengo este problema usando for loop. dictionary [index] [key] = "abc", pero revierte al valor inicial "xyz" –

3

no puede modificar las claves ni los valores directamente en un ParaCada, pero se puede modificar sus miembros. Por ejemplo, esto debería funcionar:

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

¿Qué tal hacer algunas consultas LINQ en contra de su diccionario, y luego ligar su gráfico con los resultados de los ...

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

¿No está disponible Linz solo en 3.5? Estoy usando .net 2.0. – Aheho

+0

Puedes usarlo desde 2.0 con una referencia a la versión 3.5 de System.Core.DLL. Si eso no es algo que quieras emprender, házmelo saber y borraré esta respuesta. –

+1

Probablemente no siga esta ruta, pero es una buena sugerencia, sin embargo. Te sugiero que dejes la respuesta en su lugar en caso de que alguien más con el mismo problema se tropiece con ella. – Aheho

2

Si te sientes? creativo podrías hacer algo como esto. Regrese hacia atrás a través del diccionario para hacer sus cambios.

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

} 

Ciertamente no idéntica, pero que podría estar interesado de todos modos ...

44

Llame al ToList() en el bucle foreach. De esta forma, no necesitamos una copia variable temporal. Depende de Linq, que está disponible desde .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

¡Muy buena mejora! – SpeziFish

+0

Sería mejor utilizar 'foreach (var pair en colStates.ToList()) 'para evitar tener acceso a la clave * y * el valor que evita tener que llamar a' colStates [key] '.. – user2864740

0

se puede hacer una copia de la lista de dict.Values, a continuación, puede utilizar la función List.ForEach lambda para la iteración, (o un bucle foreach, según lo sugerido antes).

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

Considerar' foreach 'para el código de producción de efectos secundarios. – user2864740

0

A partir de .NET 4.5 Usted puede hacer esto 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 sin embargo, que su rendimiento es en realidad mucho peor que un simple 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>(); 
    } 
} 

Resultado:

   Method |  Mean |  Error | StdDev | 
--------------------- |----------:|----------:|----------:| 
ConcurrentDictionary | 182.24 us | 3.1507 us | 2.7930 us | 
      Dictionary | 47.01 us | 0.4824 us | 0.4512 us |