Bir foreach döngüsünde sözlük değerlerini düzenleme


192

Bir sözlükten bir pasta grafik oluşturmaya çalışıyorum. Pasta grafiğini görüntülemeden önce verileri toplamak istiyorum. Pastanın% 5'inden daha az olan pasta dilimlerini kaldırıyorum ve bunları "Diğer" pasta dilimi içine koyuyorum. Ancak Collection was modified; enumeration operation may not executeçalışma zamanında bir istisna alıyorum .

Neden bir yineleme yaparken sözlükten öğe ekleyemeyeceğinizi veya sözlükten öğe kaldıramayacağınızı anlıyorum. Ancak neden foreach döngüsü içinde varolan bir anahtarın değerini değiştiremeyeceğinizi anlamıyorum.

Herhangi bir öneri yeniden: kodumu sabitleme, mutluluk duyacağız.

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

Yanıtlar:


262

Sözlükteki bir değer ayarlandığında, yineleyiciyi ve anahtarlar veya değerler koleksiyonuyla ilişkilendirilmiş yineleyicileri geçersiz kılan dahili "sürüm numarası" güncellenir.

Ne demek istediğini görüyorum, ama aynı zamanda değerler toplama orta-yinelemeyi değiştirebilirse garip olurdu - ve basitlik için sadece bir sürüm numarası var.

Bu tür şeyleri düzeltmenin normal yolu, anahtar koleksiyonunu önceden kopyalayıp kopya üzerinde yinelemek veya orijinal koleksiyonu yinelemek, ancak yinelemeyi bitirdikten sonra uygulayacağınız bir değişiklik koleksiyonunu korumaktır.

Örneğin:

Önce anahtarları kopyalama

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

Veya...

Değişikliklerin bir listesini oluşturma

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
Bunun eski olduğunu biliyorum, ancak .NET 3.5 (veya 4.0 mı?) Kullanıyorsanız, aşağıdaki gibi LINQ kullanabilir ve kötüye kullanabilirsiniz: foreach (colStates.Keys.ToList ()) dize anahtarı {...}
Machtyn

6
@Machtyn: Elbette - ama soru aksi kesinlikle, .NET 2.0 hakkında spesifik oldu ediyorum kullanılan LINQ var.
Jon Skeet

"Sürüm numarası" Sözlük'ün görünür durumunun bir parçası mı yoksa bir uygulama ayrıntısı mı?
Anonim Korkak

@SEinfringescopyright: Doğrudan görünmez; Sözlük geçersiz güncellenmesi gerçeği yineleyici olduğu görülebilir olsa.
Jon Skeet

Gönderen Microsoft Dokümanlar .NET framework 4.8 : foreach ifadesi sadece yazmadan, koleksiyondan değil okuma alanı sağlar numaralandırıcıya, etrafında bir sarıcı. Bu yüzden gelecekteki bir versiyonda değişebilecek bir uygulama detayı olduğunu söyleyebilirim. Görünen şey, Enumerator kullanıcısının sözleşmesini ihlal etmesidir. Ama yanılıyorum ... bir Sözlük serileştirildiğinde gerçekten görülebilir.
Anonim Korkak

81

Çağrı ToList()içinde foreachdöngü. Bu şekilde geçici değişken kopyasına ihtiyacımız yok. Net 3.5'ten beri kullanılabilen Linq'e bağlıdır.

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

Çok güzel bir gelişme!
SpeziFish

1
Aramak zorunda kalmadan foreach(var pair in colStates.ToList())Anahtar ve Değer erişimini önlemek için kullanmak daha iyi olurdu colStates[key]..
user2864740 28:17

21

Bu satırdaki koleksiyonu değiştiriyorsunuz:

colStates [anahtar] = 0;

Bunu yaparak, aslında o noktada bir şeyleri siler ve yeniden eklersiniz (IEnumerable zaten ilgili olduğu sürece).

Eğer bir düzenlerseniz üyesi Tamam olurdu depoladığınız değerinin, ancak değerini kendisi düzenliyor ve IEnumberable böyle değildir.

Kullandığım çözüm foreach döngüsünü ortadan kaldırmak ve sadece for döngüsünü kullanmaktır. Basit bir döngü, koleksiyonu etkilemeyeceğini bildiğiniz değişiklikleri kontrol etmez.

Bunu nasıl yapabileceğiniz aşağıda açıklanmıştır:

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

Ben döngü için kullanarak bu sorunu alıyorum. sözlük [dizin] [anahtar] = "abc", ancak "xyz" başlangıç ​​değerine geri döner
Nick Chan Abdullah

1
Bu koddaki düzeltme for döngüsü değildir: anahtar listesini kopyalar. (Eğer bir foreach döngüsü onu dönüştürülür olmadığı henüz çalışacak.) Döngüsü kullanarak anlamına geleceğini kullanarak çözme colStates.Keysyerine keys.
idbrii

6

Anahtarları veya değerleri doğrudan bir ForEach'ta değiştiremezsiniz, ancak üyelerini değiştirebilirsiniz. Örneğin, bu işe yaramalıdır:

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

Sözlüğünüze karşı bazı linq sorguları yapmaya ve ardından grafiğinizi bunların sonuçlarına bağlamaya ne dersiniz?

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

Linq sadece 3.5 sürümünde mevcut değil mi? .Net 2.0 kullanıyorum.
Aheho

2.0'dan System.Core.DLL 3.5 sürümüne başvurarak kullanabilirsiniz - eğer üstlenmek istediğiniz bir şey değilse bana bildirin ve bu yanıtı sileceğim.
Scott Ivey

1
Muhtemelen bu rotaya gitmeyeceğim, ama yine de iyi bir öneri. Aynı sorunu olan bir başkasının karşılaşması durumunda cevabı yerinde bırakmanızı öneririm.
Aheho

3

Yaratıcı hissediyorsanız böyle bir şey yapabilirsiniz. Değişikliklerinizi yapmak için sözlükte geriye doğru gidin.

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

}

Kesinlikle aynı değil, ama yine de ilginizi çekebilir ...


2

Yerinde değişiklik yapmak yerine eskiden yeni bir Sözlük oluşturmanız gerekir. Bir anahtar arama kullanmaktan ziyade (KeyValuePair <,> üzerinde yineleme yapın):

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

.NET 4.5 ile başlayarak bunu ConcurrentDictionary ile yapabilirsiniz :

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

Bununla birlikte, performansının basit bir işlemden çok daha kötü olduğunu unutmayın 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>();
    }
}

Sonuç:

              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

Koleksiyonu değiştiremezsiniz, değerleri bile değiştiremezsiniz. Bu vakaları kaydedip daha sonra kaldırabilirsiniz. Bu şöyle olur:

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

Sen edebilirsiniz koleksiyonu değiştirin. Sen olamaz değiştirilmiş koleksiyonuna bir yineleyici kullanmaya devam.
user2864740

0

Yasal Uyarı: Fazla C # yapmıyorum

HashTable'da depolanan DictionaryEntry nesnesini değiştirmeye çalışıyorsunuz. Hashtable yalnızca bir nesneyi saklar - DictionaryEntry örneğiniz. Anahtarı veya Değeri değiştirmek HashTable'ı değiştirmek ve numaralandırıcının geçersiz olmasına neden olmak için yeterlidir.

Döngünün dışında yapabilirsiniz:

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

önce değiştirmek istediğiniz değerlerin tüm tuşlarının bir listesini oluşturarak bu listeyi tekrarlayabilirsiniz.


0

Listesinin bir kopyasını oluşturabilir dict.Values, ardından List.ForEachyineleme için lambda işlevini kullanabilirsiniz (veya daha foreachönce önerildiği gibi bir döngü).

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

0

Diğer cevaplar ile birlikte, Sana alırsanız dikkat düşündüm sortedDictionary.Keysya sortedDictionary.Valuesile üzerlerinden sonra döngü ve foreach, ayrıca sıralanmış sırayla geçer. Bunun nedeni, bu yöntemlerin orijinal sözlük türünü koruyan nesneler döndürmesi System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionveya SortedDictionary<TKey,TValue>.ValueCollectionnesneler olmasıdır.


0

Bu cevap önerilen bir çözümü değil, iki çözümü karşılaştırmak içindir.

Önerilen diğer yanıtlar gibi başka bir liste oluşturmak yerine , döngü durdurma koşulu ve anahtarı almak için forsözlüğü kullanarak bir döngü kullanabilirsiniz .CountKeys.ElementAt(i)

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

İlk olarak bunun daha etkili olacağını düşündüm çünkü anahtar bir liste oluşturmamıza gerek yok. Bir test yaptıktan sonra fordöngü çözümünün çok daha az verimli olduğunu gördüm . Sebebi çünkü ElementAtO (n)dictionary.Keys mülkte olması, koleksiyonun başlangıcından n. Öğeye ulaşana kadar arama yapmasıdır.

Ölçek:

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

Sonuçlar:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.