c# - while - Modifica dei valori del dizionario in un ciclo foreach




while cycle in c# (8)

È possibile creare una copia di lista dei dict.Values , quindi è possibile utilizzare la funzione List.ForEach per l'iterazione, (o un ciclo foreach , come suggerito prima).

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

Sto cercando di costruire un grafico a torta da un dizionario. Prima di visualizzare il grafico a torta, voglio riordinare i dati. Sto rimuovendo qualsiasi porzione di torta che sarebbe inferiore al 5% della torta e inserendoli in una fetta di torta "Altro". Comunque sto ricevendo una Collection was modified; enumeration operation may not execute Collection was modified; enumeration operation may not execute eccezioni in fase di runtime.

Capisco perché non è possibile aggiungere o rimuovere elementi da un dizionario durante l'iterazione su di essi. Tuttavia, non capisco perché non si possa semplicemente modificare un valore per una chiave esistente all'interno del ciclo foreach.

Qualche suggerimento: correggere il mio codice, sarebbe apprezzato.

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

A partire da .NET 4.5 È possibile farlo 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);

Si noti tuttavia che le sue prestazioni sono in realtà molto peggiori di un semplice 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>();
    }
}

Risultato:

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

Chiama la ToList() nel ciclo foreach . In questo modo non abbiamo bisogno di una copia variabile temporanea. Dipende da Linq, disponibile da .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;
    }
}

Devi creare un nuovo dizionario dal vecchio piuttosto che modificarlo sul posto. Qualcosa come (anche iterare su KeyValuePair <,> piuttosto che usare una ricerca chiave:

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;

L'impostazione di un valore in un dizionario aggiorna il suo "numero di versione" interno, che invalida l'iteratore e qualsiasi iteratore associato alle chiavi o alla raccolta di valori.

Vedo il tuo punto, ma allo stesso tempo sarebbe strano se la collezione di valori potesse cambiare a metà iterazione e per semplicità c'è solo un numero di versione.

Il modo normale di risolvere questo tipo di cose è copiare la collezione di chiavi in ​​anticipo e iterare sulla copia, o scorrere sulla collezione originale mantenendo una collezione di modifiche che applicherete dopo aver terminato l'iterazione.

Per esempio:

Copia le chiavi prima

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

Creazione di un elenco di modifiche

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

Non è possibile modificare la raccolta, nemmeno i valori. Potresti salvare questi casi e rimuoverli in seguito. Finirebbe così:

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

Se ti senti creativo potresti fare qualcosa di simile. Fai scorrere all'indietro il dizionario per apportare le modifiche.

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

}

Certamente non identico, ma potreste essere interessati comunque ...


Stai modificando la raccolta in questa riga:

colStates [chiave] = 0;

In questo modo, si sta essenzialmente eliminando e reinserendo qualcosa in quel punto (per quanto riguarda comunque IEnumerable.

Se modifichi un membro del valore che stai memorizzando, sarebbe OK, ma stai modificando il valore stesso e IEnumberable non gli piace.

La soluzione che ho usato è quella di eliminare il ciclo foreach e utilizzare solo un ciclo for. Un ciclo for semplice non verificherà le modifiche che sapete non influiranno sulla raccolta.

Ecco come puoi farlo:

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






.net-2.0