c# - Editar valores de diccionario en un bucle foreach
.net .net-2.0 (11)
¿Qué tal si hace algunas consultas linq contra su diccionario y luego vincula su gráfico con los resultados de esos? ...
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);
}
Estoy tratando de construir 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, estoy obteniendo una Collection was modified; enumeration operation may not execute
Collection was modified; enumeration operation may not execute
excepción en el tiempo de ejecución.
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);
Al establecer un valor en un diccionario, se actualiza su "número de versión" interno, lo que invalida el iterador y cualquier iterador asociado con la colección de claves o valores.
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 arreglar este tipo de cosas es copiar la colección de claves de antemano 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 de iterar.
Por ejemplo:
Primero copiando claves
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...
Creando 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;
}
Comenzando con .NET 4.5 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);
Sin embargo, tenga en cuenta que su rendimiento es mucho peor que un 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 |
Descargo de responsabilidad: no hago mucho C #
Está intentando modificar el objeto DictionaryEntry que está almacenado en la HashTable. El Hashtable solo almacena un objeto: su instancia de DictionaryEntry. Cambiar la clave o el valor es suficiente para cambiar la tabla Hash y hacer que el enumerador se vuelva inválido.
Puedes 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 itere a través de esa lista en su lugar.
Está modificando la colección en esta línea:
colStates [clave] = 0;
Al hacerlo, básicamente estás eliminando y reinsertando algo en ese punto (en lo que concierne a IEnumerable).
Si edita un miembro del valor que está almacenando, eso estaría bien, 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 bucle for. Un ciclo for simple no verificará los cambios que usted sabe que no afectarán a la colección.
He aquí cómo puedes 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;
}
}
Llame a 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;
}
}
Necesita crear un nuevo diccionario a partir de lo antiguo en lugar de modificarlo en su lugar. Algo parecido a Somethine (también iterar sobre KeyValuePair <,> en lugar de usar una búsqueda de clave:
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;
No puede modificar la colección, ni siquiera los valores. Puede guardar estos casos y eliminarlos más tarde. Terminaría así:
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);
No puede modificar las claves ni los valores directamente en ForEach, pero 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 } );
Puede hacer una copia de lista de los dict.Values
, luego puede usar la función List.ForEach
lambda para la iteración (o un bucle foreach
, como se sugirió anteriormente).
new List<string>(myDict.Values).ForEach(str =>
{
//Use str in any other way you need here.
Console.WriteLine(str);
});
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 es idéntico, pero de todos modos podría estar interesado ...