Modification des valeurs du dictionnaire dans une boucle foreach

J'essaie de construire un camembert à partir d'un dictionnaire. Avant d'afficher le camembert, je veux ranger les données. Je supprime toutes les tranches de tarte qui seraient inférieures à 5% de la tarte et les mets dans une "autre" tranche de tarte. Cependant, je reçois une exception Collection was modified; enumeration operation may not execute à l'exécution.

Je comprends pourquoi vous ne pouvez pas ajouter ou supprimer des éléments d'un dictionnaire tout en les itérant. Cependant je ne comprends pas pourquoi vous ne pouvez pas simplement changer une valeur pour une clé existante dans le foreach boucle.

Toutes les suggestions concernant: la fixation 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);
159
demandé sur sǝɯɐſ 2009-07-01 23:00:50

12 réponses

Définir une valeur dans un dictionnaire met à jour son "numéro de version" interne - ce qui invalide l'itérateur et tout itérateur associé à la collection de clés ou de valeurs.

Je vois votre point, mais en même temps, il serait étrange que la collection de valeurs puisse changer à mi-itération - et pour simplifier, il n'y a qu'un seul numéro de version.

La façon normale de réparer ce genre de chose est de copier la collection de clés à l'avance et d'itérer sur la copie, ou d'itérer sur la collection d'origine, mais maintenez une collection de modifications que vous appliquerez après avoir terminé l'itération.

Par exemple:

Copie des clés en premier

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éation d'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;
}
217
répondu Jon Skeet 2013-05-23 18:50:43

Appelez le 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;
    }
}
52
répondu DIG 2016-11-16 02:23:34

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 modifiez un membre de la valeur stockée, ce serait OK, mais vous modifiez la valeur elle-même et IEnumberable n'aime pas cela.

La solution que j'ai utilisée est d'éliminer la boucle foreach et d'utiliser simplement une boucle for. Un simple pour la boucle ne vérifiera pas les changements que vous savez n'affectera pas la collection.

Voici comment vous pourriez 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;    
    }
}
17
répondu CodeFusionMobile 2009-07-01 19:19:02

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
répondu Jeremy Frey 2009-07-01 19:15:43

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
répondu Scott Ivey 2009-07-01 19:18:57

Si vous vous sentez créatif, vous pourriez faire quelque chose comme ça. Faites une boucle dans le dictionnaire pour effectuer 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é de toute façon...

2
répondu Hugoware 2009-07-01 19:19:08

Vous devez créer un nouveau dictionnaire à partir de l'ancien plutôt que de le modifier en place. Quelque chose comme (aussi itérer sur le KeyValuePair plutôt que d'utiliser une recherche de 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;
1
répondu Richard 2009-07-01 19:08:43

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

        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
répondu Samuel Carrijo 2009-07-01 19:08:59

Avertissement: Je ne fais pas beaucoup C #

Vous essayez de modifier L'objet DictionaryEntry qui est stocké dans la table de hachage. La table de hachage ne stocke qu'un seul objet-votre instance de DictionaryEntry. La modification de la clé ou de la valeur suffit à modifier la table de hachage 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 parcourir cette liste plutôt.

0
répondu Cambium 2009-07-01 19:08:58

Vous pouvez faire une copie de liste du dict.Values, puis vous pouvez utiliser la fonction List.ForEach lambda 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
répondu Nick Louloudakis 2015-07-10 14:00:16

À 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 cependant que ses performances sont en fait 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
répondu Ohad Schneider 2017-08-29 15:49:29

Avec les autres réponses, je pensais notez que si vous obtenez sortedDictionary.Keys ou sortedDictionary.Values et ensuite une boucle avec foreach, vous aussi passer dans l'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 le genre du dictionnaire d'origine.

0
répondu jep 2018-08-15 14:52:58