Bearbeiten von Wörterbuchwerten in einer foreach-Schleife


191

Ich versuche, ein Kreisdiagramm aus einem Wörterbuch zu erstellen. Bevor ich das Kreisdiagramm anzeige, möchte ich die Daten aufräumen. Ich entferne alle Kuchenstücke, die weniger als 5% des Kuchens ausmachen, und lege sie in ein "anderes" Kuchenstück. Allerdings bekomme ich Collection was modified; enumeration operation may not executezur Laufzeit eine Ausnahme.

Ich verstehe, warum Sie keine Elemente zu einem Wörterbuch hinzufügen oder daraus entfernen können, während Sie darüber iterieren. Ich verstehe jedoch nicht, warum Sie einen Wert für einen vorhandenen Schlüssel in der foreach-Schleife nicht einfach ändern können.

Vorschläge zur Korrektur meines Codes sind willkommen.

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

Antworten:


259

Durch das Festlegen eines Werts in einem Wörterbuch wird die interne "Versionsnummer" aktualisiert, wodurch der Iterator und alle mit der Schlüssel- oder Wertsammlung verknüpften Iteratoren ungültig werden.

Ich verstehe Ihren Standpunkt, aber gleichzeitig wäre es seltsam, wenn sich die Wertsammlung während der Iteration ändern könnte - und der Einfachheit halber gibt es nur eine Versionsnummer.

Die normale Methode zur Behebung dieser Probleme besteht darin, entweder die Schlüsselsammlung im Voraus zu kopieren und über die Kopie zu iterieren oder die ursprüngliche Sammlung zu durchlaufen, aber eine Sammlung von Änderungen beizubehalten, die Sie nach Abschluss der Iteration anwenden.

Beispielsweise:

Schlüssel zuerst kopieren

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

Oder...

Erstellen einer Liste mit Änderungen

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

24
Ich weiß, dass dies alt ist, aber wenn Sie .NET 3.5 verwenden (oder 4.0?), Können Sie LINQ wie folgt verwenden und missbrauchen: foreach (Zeichenfolgenschlüssel in colStates.Keys.ToList ()) {...}
Machtyn

6
@Machtyn: Sicher - aber die Frage war speziell über .NET 2.0, sonst sicher ich würde gebrauchte LINQ hat.
Jon Skeet

Ist die "Versionsnummer" Teil des sichtbaren Status des Wörterbuchs oder ein Implementierungsdetail?
Anonymer Feigling

@ SEinfringescopyright: Es ist nicht direkt sichtbar; Die Tatsache, dass das Aktualisieren des Wörterbuchs den Iterator ungültig macht, ist jedoch sichtbar.
Jon Skeet

Aus Microsoft Docs .NET Framework 4.8 : Die foreach-Anweisung ist ein Wrapper um den Enumerator, der nur das Lesen aus der Sammlung und nicht das Schreiben in die Auflistung ermöglicht. Ich würde also sagen, dass es sich um ein Implementierungsdetail handelt, das sich in einer zukünftigen Version ändern könnte. Und was sichtbar ist, ist, dass der Benutzer des Enumerators seinen Vertrag verletzt hat. Aber ich würde mich irren ... es ist wirklich sichtbar, wenn ein Wörterbuch serialisiert wird.
Anonymer Feigling

81

Rufen Sie die ToList()in derforeach Schleife. Auf diese Weise benötigen wir keine temporäre Variablenkopie. Dies hängt von Linq ab, das seit .Net 3.5 verfügbar ist.

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

Sehr schöne Verbesserung!
SpeziFish

1
Es wäre besser zu verwenden foreach(var pair in colStates.ToList()), um den Zugriff auf den Schlüssel und den Wert zu vermeiden, wodurch vermieden werden muss, dass colStates[key]..
user2864740

21

Sie ändern die Sammlung in dieser Zeile:

colStates [Schlüssel] = 0;

Auf diese Weise löschen Sie an dieser Stelle im Wesentlichen etwas und fügen es wieder ein (soweit es IEnumerable betrifft.

Wenn Sie ein Mitglied bearbeiten des Wertes den Sie speichern, wäre das in Ordnung, aber Sie bearbeiten den Wert selbst und IEnumberable gefällt das nicht.

Die Lösung, die ich verwendet habe, besteht darin, die foreach-Schleife zu entfernen und nur eine for-Schleife zu verwenden. Eine einfache for-Schleife sucht nicht nach Änderungen, von denen Sie wissen, dass sie die Sammlung nicht beeinflussen.

So könnten Sie es machen:

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

Ich bekomme dieses Problem mit for loop. dictionary [index] [key] = "abc", aber es kehrt zum Anfangswert "xyz" zurück
Nick Chan Abdullah

1
Das Update in diesem Code ist nicht die for-Schleife: Es kopiert die Liste der Schlüssel. (Es würde immer noch funktionieren, wenn Sie es in eine foreach-Schleife konvertieren würden.) Das Lösen mit einer for-Schleife würde die Verwendung colStates.Keysvon anstelle von bedeuten keys.
idbrii

6

Sie können weder die Schlüssel noch die Werte direkt in einem ForEach ändern, aber Sie können deren Mitglieder ändern. ZB sollte das funktionieren:

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

3

Wie wäre es, wenn Sie nur einige Linq-Abfragen für Ihr Wörterbuch durchführen und dann Ihr Diagramm an die Ergebnisse dieser Wörter binden? ...

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

Ist Linq nicht nur in 3.5 verfügbar? Ich benutze .net 2.0.
Aheho

Sie können es ab 2.0 mit einem Verweis auf die 3.5-Version von System.Core.DLL verwenden. Wenn Sie dies nicht möchten, lassen Sie es mich wissen und ich werde diese Antwort löschen.
Scott Ivey

1
Ich werde diesen Weg wahrscheinlich nicht gehen, aber es ist trotzdem ein guter Vorschlag. Ich schlage vor, Sie lassen die Antwort an Ort und Stelle, falls jemand anderes mit dem gleichen Problem darauf stößt.
Aheho

3

Wenn Sie sich kreativ fühlen, können Sie so etwas tun. Durchlaufen Sie das Wörterbuch rückwärts, um Ihre Änderungen vorzunehmen.

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

}

Sicher nicht identisch, aber Sie könnten trotzdem interessiert sein ...


2

Sie müssen ein neues Wörterbuch aus dem alten erstellen, anstatt es zu ändern. Etwas wie (iterieren Sie auch über das KeyValuePair <,> anstatt eine Schlüsselsuche zu verwenden:

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

Beginnend mit .NET 4.5 Mit ConcurrentDictionary können Sie dies tun :

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

Beachten Sie jedoch, dass seine Leistung tatsächlich viel schlechter ist als eine einfache 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>();
    }
}

Ergebnis:

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

1

Sie können die Sammlung nicht ändern, nicht einmal die Werte. Sie können diese Fälle speichern und später entfernen. Es würde so enden:

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

Sie können die Sammlung ändern. Sie können keinen Iterator für eine geänderte Sammlung verwenden.
user2864740

0

Haftungsausschluss: Ich mache nicht viel C #

Sie versuchen, das DictionaryEntry-Objekt zu ändern, das in der HashTable gespeichert ist. In der Hashtabelle wird nur ein Objekt gespeichert - Ihre Instanz von DictionaryEntry. Das Ändern des Schlüssels oder des Werts reicht aus, um die HashTable zu ändern und den Enumerator ungültig zu machen.

Sie können dies außerhalb der Schleife tun:

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

Erstellen Sie zunächst eine Liste aller Schlüssel der Werte, die Sie ändern möchten, und durchlaufen Sie stattdessen diese Liste.


0

Sie können eine Listenkopie von erstellen dict.Valuesund dann die List.ForEachLambda-Funktion für die Iteration verwenden (oder eine foreachSchleife, wie zuvor vorgeschlagen).

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

0

Zusammen mit den anderen Antworten dachte ich, ich würde beachten, dass Sie, wenn Sie sie erhalten sortedDictionary.Keysoder sortedDictionary.Valuesdann foreachdurchlaufen, sie auch in sortierter Reihenfolge durchlaufen. Dies liegt daran, dass diese Methoden System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionoder SortedDictionary<TKey,TValue>.ValueCollectionObjekte zurückgeben, die die Art des ursprünglichen Wörterbuchs beibehalten.


0

Diese Antwort dient zum Vergleich zweier Lösungen, nicht als Lösungsvorschlag.

Anstatt eine andere Liste zu erstellen, wie in anderen Antworten vorgeschlagen, können Sie eine forSchleife verwenden, die das Wörterbuch Countfür die Schleifenstoppbedingung und Keys.ElementAt(i)zum Abrufen des Schlüssels verwendet.

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

Bei firs dachte ich, das wäre effizienter, weil wir keine Schlüsselliste erstellen müssen. Nach einem Test stellte ich fest, dass die forSchleifenlösung viel weniger ist effizient ist. Bei Liste auf meinem PC.

Prüfung:

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

Ergebnisse:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.