Modification des valeurs de dictionnaire dans une boucle foreach

192

J'essaie de créer un graphique à secteurs à partir d'un dictionnaire. Avant d'afficher le graphique à secteurs, je souhaite ranger les données. Je supprime toutes les tranches de tarte qui représenteraient moins de 5% de la tarte et les mets dans une tranche de tarte «Autre». Cependant, je reçois une Collection was modified; enumeration operation may not executeexception au moment de l'exécution.

Je comprends pourquoi vous ne pouvez pas ajouter ou supprimer des éléments d'un dictionnaire lors de leur itération. Cependant, je ne comprends pas pourquoi vous ne pouvez pas simplement modifier une valeur pour une clé existante dans la boucle foreach.

Toute suggestion concernant la fixation de mon code serait appréciée.

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);
Aheho
la source

Réponses:

262

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

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

La manière normale de résoudre ce genre de problème consiste soit à copier la collection de clés au préalable et à parcourir la copie, soit à parcourir la collection d'origine tout en conservant une collection de modifications que vous appliquerez après avoir terminé l'itération.

Par exemple:

Copier d'abord les clés

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;
}
Jon Skeet
la source
24
Je sais que c'est vieux, mais si vous utilisez .NET 3.5 (ou est-ce 4.0?), Vous pouvez utiliser et abuser de LINQ comme suit: foreach (string key in colStates.Keys.ToList ()) {...}
Machtyn
6
@Machtyn: Bien sûr - mais la question était précisément de .NET 2.0, sinon je certainement aurais avoir utilisé LINQ.
Jon Skeet
Le "numéro de version" fait-il partie de l'état visible du dictionnaire ou d'un détail d'implémentation?
Anonymous Coward le
@SEinfringescopyright: il n'est pas visible directement; le fait que la mise à jour du dictionnaire invalide l'itérateur est cependant visible.
Jon Skeet le
À partir de Microsoft Docs .NET Framework 4.8 : l' instruction foreach est un wrapper autour de l'énumérateur, qui permet uniquement de lire à partir de la collection, et non d'y écrire. Je dirais donc que c'est un détail d'implémentation qui pourrait changer dans une future version. Et ce qui est visible, c'est que l'utilisateur de l'énumérateur a rompu son contrat. Mais je me trompe ... c'est vraiment visible quand un dictionnaire est sérialisé.
Anonymous Coward le
81

Appelez le ToList()dans la foreachboucle. De cette façon, nous n'avons pas besoin d'une copie de la 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;
    }
}
CREUSER
la source
Très belle amélioration!
SpeziFish
1
Il serait préférable de l'utiliser foreach(var pair in colStates.ToList())pour éviter d'avoir accès à la clé et à la valeur ce qui évite d'avoir à appeler en colStates[key]..
user2864740
21

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 que vous stockez, ce serait OK, mais vous modifiez la valeur elle-même et IEnumberable n'aime pas ça.

La solution que j'ai utilisée est d'éliminer la boucle foreach et d'utiliser simplement une boucle for. Une simple boucle for ne vérifiera pas les modifications dont vous savez qu'elles n'affecteront pas la collection.

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;    
    }
}
CodeFusionMobile
la source
J'obtiens ce problème en utilisant la boucle for. dictionary [index] [key] = "abc", mais il revient à la valeur initiale "xyz"
Nick Chan Abdullah
1
Le correctif dans ce code n'est pas la boucle for: il copie la liste des clés. (Cela fonctionnerait toujours si vous le convertissiez en boucle foreach.) Résoudre en utilisant une boucle for signifierait utiliser colStates.Keysà la place de keys.
idbrii
6

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 } );
Jeremy Frey
la source
3

Que diriez-vous de faire quelques requêtes linq sur votre dictionnaire, puis de lier votre graphique aux résultats de celles-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);
}
Scott Ivey
la source
Linq n'est-il pas uniquement disponible en 3.5? J'utilise .net 2.0.
Aheho
Vous pouvez l'utiliser à partir de 2.0 avec une référence à la version 3.5 de System.Core.DLL - si ce n'est pas quelque chose que vous souhaitez entreprendre, faites-le moi savoir et je supprimerai cette réponse.
Scott Ivey
1
Je n'irai probablement pas dans cette voie, mais c'est néanmoins une bonne suggestion. Je vous suggère de laisser la réponse en place au cas où quelqu'un d'autre avec le même problème tomberait dessus.
Aheho le
3

Si vous vous sentez créatif, vous pouvez 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é quand même ...

Hugoware
la source
2

Vous devez créer un nouveau dictionnaire à partir de l'ancien plutôt que de le modifier sur place. Quelque chose comme (également itérer sur 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;
Richard
la source
1

À 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 réalité 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 |
Ohad Schneider
la source
1

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);
Samuel Carrijo
la source
Vous pouvez modifier la collection. Vous ne pouvez pas continuer à utiliser un itérateur pour une collection modifiée.
user2864740
0

Avertissement: je ne fais pas beaucoup de C #

Vous essayez de modifier l'objet DictionaryEntry qui est stocké dans le HashTable. Le Hashtable ne stocke qu'un seul objet - votre instance de DictionaryEntry. La modification de la clé ou de la valeur suffit à modifier 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 en parcourant cette liste à la place.

Cambium
la source
0

Vous pouvez faire une copie de liste de dict.Values, puis vous pouvez utiliser la List.ForEachfonction lambda pour l'itération, (ou une foreachboucle, 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);
});
Nick L.
la source
0

En plus des autres réponses, j'ai pensé que je noterais que si vous obtenez sortedDictionary.Keysou sortedDictionary.Valueset que foreachvous les parcourez ensuite, vous les parcourez également dans un ordre trié. En effet, ces méthodes renvoient des objets System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionou SortedDictionary<TKey,TValue>.ValueCollectionqui conservent le type du dictionnaire d'origine.

jep
la source
0

Cette réponse est pour comparer deux solutions, pas une solution suggérée.

Au lieu de créer une autre liste comme d'autres réponses le suggèrent, vous pouvez utiliser une forboucle en utilisant le dictionnaire Countpour la condition d'arrêt de boucle et Keys.ElementAt(i)pour obtenir la clé.

for (int i = 0; i < dictionary.Count; i++)
{
    dictionary[dictionary.Keys.ElementAt(i)] = 0;
}

Au début, j'ai pensé que ce serait plus efficace car nous n'avons pas besoin de créer une liste de clés. Après avoir exécuté un test, j'ai trouvé que la forsolution de boucle est beaucoup moins efficace. La raison en est que ElementAtO (n) sur ledictionary.Keys propriété, il recherche depuis le début de la collection jusqu'à ce qu'il atteigne le nième élément.

Tester:

int iterations = 10;
int dictionarySize = 10000;
Stopwatch sw = new Stopwatch();

Console.WriteLine("Creating dictionary...");
Dictionary<string, int> dictionary = new Dictionary<string, int>(dictionarySize);
for (int i = 0; i < dictionarySize; i++)
{
    dictionary.Add(i.ToString(), i);
}
Console.WriteLine("Done");

Console.WriteLine("Starting tests...");

// for loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    for (int j = 0; j < dictionary.Count; j++)
    {
        dictionary[dictionary.Keys.ElementAt(j)] = 3;
    }
}
sw.Stop();
Console.WriteLine($"for loop Test:     {sw.ElapsedMilliseconds} ms");

// foreach loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    foreach (string key in dictionary.Keys.ToList())
    {
        dictionary[key] = 3;
    }
}
sw.Stop();
Console.WriteLine($"foreach loop Test: {sw.ElapsedMilliseconds} ms");

Console.WriteLine("Done");

Résultats:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
Eliahu Aaron
la source