Mengedit nilai-nilai kamus dalam loop foreach

192

Saya mencoba membuat diagram lingkaran dari kamus. Sebelum saya menampilkan diagram lingkaran, saya ingin merapikan datanya. Saya menghapus setiap irisan pai yang kurang dari 5% dari pai dan menempatkannya dalam irisan pai "Lainnya". Namun saya mendapatkan Collection was modified; enumeration operation may not executepengecualian saat runtime.

Saya mengerti mengapa Anda tidak dapat menambah atau menghapus item dari kamus saat iterasi. Namun saya tidak mengerti mengapa Anda tidak bisa begitu saja mengubah nilai untuk kunci yang ada dalam loop foreach.

Setiap saran ulang: memperbaiki kode saya, akan dihargai.

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
sumber

Jawaban:

262

Menetapkan nilai dalam kamus akan memperbarui "nomor versi" internalnya - yang membatalkan iterator, dan iterator apa pun yang terkait dengan kunci atau kumpulan nilai.

Saya mengerti maksud Anda, tetapi pada saat yang sama akan aneh jika pengumpulan nilai dapat mengubah iterasi tengah - dan untuk kesederhanaan hanya ada satu nomor versi.

Cara normal untuk memperbaiki hal semacam ini adalah dengan menyalin koleksi kunci sebelumnya dan beralih di atas salinan, atau beralih ke koleksi asli tetapi mempertahankan koleksi perubahan yang akan Anda terapkan setelah Anda selesai iterasi.

Sebagai contoh:

Menyalin kunci terlebih dahulu

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

Atau...

Membuat daftar modifikasi

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
sumber
24
Saya tahu ini sudah tua, tetapi jika menggunakan. NET 3.5 (atau 4.0?) Anda dapat menggunakan dan menyalahgunakan LINQ sebagai berikut: foreach (kunci string di colStates.Keys.ToList ()) {...}
Machtyn
6
@Machtyn: Tentu - tapi pertanyaannya secara khusus tentang .NET 2.0, kalau tidak saya pasti akan menggunakan LINQ.
Jon Skeet
Apakah "nomor versi" adalah bagian dari keadaan Kamus yang terlihat atau detail implementasi?
Anonymous Coward
@SEinfringescopyright: Tidak terlihat secara langsung; fakta bahwa memperbarui membatalkan kamus iterator adalah meskipun terlihat.
Jon Skeet
Dari Microsoft Docs .NET framework 4.8 : Pernyataan foreach adalah pembungkus di sekitar enumerator, yang memungkinkan hanya membaca dari koleksi, bukan menulis untuk itu. Jadi saya akan mengatakan bahwa ini adalah detail implementasi yang dapat berubah di versi masa depan. Dan yang terlihat adalah bahwa pengguna Enumerator telah melanggar kontraknya. Tapi saya akan salah ... itu benar-benar terlihat ketika suatu Kamus berseri.
Anonymous Coward
81

Panggil ToList()di dalam foreachloop. Dengan cara ini kita tidak memerlukan salinan variabel temp. Itu tergantung pada Linq yang tersedia sejak. 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;
    }
}
MENGGALI
sumber
Peningkatan yang sangat bagus!
SpeziFish
1
Akan lebih baik untuk menggunakan foreach(var pair in colStates.ToList())untuk menghindari memiliki akses ke Kunci dan Nilai yang menghindari harus memanggil colStates[key]..
user2864740
21

Anda mengubah koleksi di baris ini:

colStates [key] = 0;

Dengan melakukannya, Anda pada dasarnya menghapus dan memasukkan kembali sesuatu pada titik itu (sejauh menyangkut IEnumerable.

Jika Anda mengedit anggota dari nilai yang Anda simpan, itu akan baik-baik saja, tetapi Anda mengedit nilai itu sendiri dan IEnumberable tidak menyukainya.

Solusi yang saya gunakan adalah menghilangkan foreach loop dan cukup gunakan for loop. Simpul untuk loop tidak akan memeriksa perubahan yang Anda tahu tidak akan memengaruhi koleksi.

Inilah cara Anda dapat melakukannya:

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
sumber
Saya mendapatkan masalah ini menggunakan for loop. dictionary [index] [key] = "abc", tetapi kembali ke nilai awal "xyz"
Nick Chan Abdullah
1
Perbaikan dalam kode ini bukan untuk loop: itu menyalin daftar kunci. (Masih akan bekerja jika Anda mengonversinya ke loop foreach.) Memecahkan dengan menggunakan for loop berarti menggunakan colStates.Keysdi tempat keys.
idbrii
6

Anda tidak dapat mengubah kunci atau nilai-nilai secara langsung dalam ForEach, tetapi Anda dapat memodifikasi anggota mereka. Misalnya, ini seharusnya bekerja:

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
sumber
3

Bagaimana kalau hanya melakukan beberapa permintaan LINQ terhadap kamus Anda, dan kemudian mengikat grafik Anda ke hasil dari mereka? ...

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
sumber
Bukankah Linq hanya tersedia dalam 3,5? Saya menggunakan .net 2.0.
Aheho
Anda dapat menggunakannya dari 2.0 dengan referensi ke versi 3.5 System.Core.DLL - jika itu bukan sesuatu yang ingin Anda lakukan, beri tahu saya dan saya akan menghapus jawaban ini.
Scott Ivey
1
Saya mungkin tidak akan menempuh rute ini, tetapi itu saran yang bagus. Saya sarankan Anda meninggalkan jawabannya di tempat seandainya ada orang lain dengan masalah yang sama tersandung di atasnya.
Aheho
3

Jika Anda merasa kreatif, Anda bisa melakukan sesuatu seperti ini. Ulangi kamus untuk melakukan perubahan.

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

}

Tentu saja tidak identik, tetapi Anda mungkin tertarik ...

Hugoware
sumber
2

Anda perlu membuat Kamus baru dari yang lama alih-alih memodifikasi di tempat. Sesuatu seperti (juga beralih pada KeyValuePair <,> daripada menggunakan pencarian kunci:

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
sumber
1

Dimulai dengan .NET 4.5 Anda dapat melakukan ini dengan 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);

Namun perlu dicatat bahwa kinerjanya sebenarnya jauh lebih buruk daripada yang sederhana 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>();
    }
}

Hasil:

              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
sumber
1

Anda tidak dapat mengubah koleksi, bahkan nilainya. Anda dapat menyimpan kasus-kasus ini dan menghapusnya nanti. Itu akan berakhir seperti ini:

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
sumber
Anda dapat mengubah koleksi. Anda tidak dapat terus menggunakan iterator ke koleksi yang dimodifikasi.
user2864740
0

Penafian: Saya tidak berbuat banyak C #

Anda mencoba untuk memodifikasi objek DictionaryEntry yang disimpan di HashTable. Hashtable hanya menyimpan satu objek - instance Anda dari DictionaryEntry. Mengubah Kunci atau Nilai sudah cukup untuk mengubah HashTable dan menyebabkan enumerator menjadi tidak valid.

Anda dapat melakukannya di luar loop:

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

dengan terlebih dahulu membuat daftar semua kunci dari nilai yang ingin Anda ubah dan beralih melalui daftar itu.

Cambium
sumber
0

Anda dapat membuat daftar salinan dict.Values, kemudian Anda dapat menggunakan List.ForEachfungsi lambda untuk iterasi, (atau foreachloop, seperti yang disarankan sebelumnya).

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

Bersamaan dengan jawaban yang lain, saya pikir saya akan perhatikan bahwa jika Anda mendapatkan sortedDictionary.Keysatau sortedDictionary.Valueskemudian mengulanginya foreach, Anda juga harus mengurutkannya. Ini karena metode tersebut mengembalikan System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionatau SortedDictionary<TKey,TValue>.ValueCollectionobjek, yang mempertahankan semacam kamus asli.

jep
sumber
0

Jawaban ini untuk membandingkan dua solusi, bukan solusi yang disarankan.

Alih-alih membuat daftar lain seperti jawaban yang disarankan, Anda dapat menggunakan forloop menggunakan kamus Countuntuk kondisi stop loop dan Keys.ElementAt(i)untuk mendapatkan kunci.

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

Pada awalnya saya pikir ini akan lebih efisien karena kita tidak perlu membuat daftar kunci. Setelah menjalankan tes saya menemukan bahwa forsolusi loop jauh lebih efisien. Alasannya adalah karena ElementAtO (n) padadictionary.Keys properti, ia mencari dari awal koleksi hingga sampai ke item ke-n.

Uji:

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

Hasil:

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