web-dev-qa-db-fra.com

Modification des valeurs du dictionnaire dans une boucle foreach

J'essaye de construire un camembert à partir d'un dictionnaire. Avant d’afficher le graphique à secteurs, je souhaite ranger les données. J'enlève toutes les tranches de tarte qui représentent moins de 5% de la tarte et je les mets dans une tranche de tarte "Autre". Cependant, je reçois une exception Collection was modified; enumeration operation may not execute au moment de l'exécution.

Je comprends pourquoi vous ne pouvez pas ajouter ou supprimer des éléments d’un dictionnaire en les parcourant. Cependant, je ne comprends pas pourquoi vous ne pouvez pas simplement changer une valeur pour une clé existante dans la boucle foreach.

Toutes les suggestions concernant la réparation de mon code seraient appréciées.

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

La définition d'une valeur dans un dictionnaire met à jour son "numéro de version" interne, qui invalide l'itérateur, ainsi que tout itérateur associé à la collection de clés ou de valeurs.

Je comprends votre point de vue, mais en même temps, il serait étrange que la collection de valeurs puisse changer à mi-parcours - et pour simplifier, il n’ya qu’un seul numéro de version.

La manière habituelle de corriger ce type de problème est de copier la collection de clés au préalable et de la parcourir par-dessus la copie, ou de parcourir la collection d'origine en conservant une collection de modifications que vous appliquerez une fois l'itération terminée.

Par exemple:

Copier les clés d'abord

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

Ou...

Créer une liste de modifications

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;
}
227
Jon Skeet

Appelez la ToList() dans la boucle foreach. De cette façon, nous n’avons pas besoin d’une copie de variable temporaire. Cela dépend de Linq qui est disponible depuis .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;
    }
}
60
DIG

Vous modifiez la collection dans cette ligne:

colStates [clé] = 0;

Ce faisant, vous supprimez et réinsérez essentiellement quelque chose à ce stade (en ce qui concerne IEnumerable de toute façon.

Si vous éditez un membre de la valeur que vous stockez, ce serait OK, mais vous éditez la valeur elle-même et IEnumberable n'aime pas cela.

La solution que j'ai utilisée consiste à éliminer la boucle foreach et à utiliser simplement une boucle for . Une simple boucle for ne vérifie pas les modifications qui, comme vous le savez, n'affecteront pas la collecte.

Voici comment vous pouvez le faire:

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

Vous ne pouvez pas modifier les clés ni les valeurs directement dans un ForEach, mais vous pouvez modifier leurs membres. Par exemple, cela devrait fonctionner:

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 } );
4
Jeremy Frey

Que diriez-vous de faire quelques requêtes linq contre votre dictionnaire, puis de lier votre graphique aux résultats de ceux-ci? ...

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);
}
3
Scott Ivey

Si vous vous sentez créatif, vous pouvez faire quelque chose comme ça. Parcourez le dictionnaire en arrière pour apporter vos modifications.

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

}

Certainement pas identique, mais vous pourriez être intéressé quand même ...

3
Hugoware

Vous devez créer un nouveau dictionnaire à partir de l'ancien plutôt que de le modifier. Quelque chose comme (également itérer sur le KeyValuePair <,> plutôt que d'utiliser une recherche clé:

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;
2
Richard

Vous ne pouvez pas modifier la collection, pas même les valeurs. Vous pouvez enregistrer ces cas et les supprimer plus tard. Ça finirait comme ça:

        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);
1
Samuel Carrijo

En même temps que les autres réponses, j'ai pensé noter que si vous obtenez sortedDictionary.Keys ou sortedDictionary.Values et que vous les parcourez ensuite avec foreach, vous passez également par ordre de tri. En effet, ces méthodes renvoient des objets System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollection ou SortedDictionary<TKey,TValue>.ValueCollection, qui conservent la sorte du dictionnaire d'origine.

0
jep

Vous pouvez créer une copie de liste du dict.Values, puis utiliser la fonction lambda List.ForEach pour l'itération (ou une boucle foreach comme suggéré précédemment).

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

À partir de .NET 4.5 Vous pouvez le faire avec 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);

Notez toutefois que ses performances sont bien pires qu’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>();
    }
}

Résultat:

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

Disclaimer: Je ne fais pas beaucoup C #

Vous essayez de modifier l'objet DictionaryEntry qui est stocké dans HashTable. La table de hachage ne stocke qu'un seul objet, votre instance de DictionaryEntry. Changer la clé ou la valeur suffit pour changer le HashTable et rendre l'énumérateur invalide.

Vous pouvez le faire en dehors de la boucle:

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

en créant d'abord une liste de toutes les clés des valeurs que vous souhaitez modifier et parcourez-la plutôt.

0
Cambium