web-dev-qa-db-ja.com

foreachループで辞書の値を編集する

辞書から円グラフを作成しようとしています。円グラフを表示する前に、データを整理したいと思います。パイの5%未満のパイスライスを削除し、「その他」のパイスライスに入れます。ただし、実行時にCollection was modified; enumeration operation may not execute例外が発生します。

繰り返し処理中に辞書の項目を追加または削除できない理由を理解しています。ただし、foreachループ内で既存のキーの値を単純に変更できない理由はわかりません。

私のコードを修正するすべての提案は、いただければ幸いです。

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

ディクショナリに値を設定すると、内部の「バージョン番号」が更新されます。これにより、反復子、およびキーまたは値のコレクションに関連付けられた反復子が無効になります。

私はあなたのポイントを見ますが、同時に値のコレクションが反復の途中で変更される可能性があるのは奇妙です-簡単にするために、バージョン番号は1つだけです。

この種の問題を修正する通常の方法は、事前にキーのコレクションをコピーしてコピーを反復処理するか、元のコレクションを反復処理して変更のコレクションを維持し、反復の完了後に適用することです。

例えば:

最初にキーをコピーする

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

または...

変更リストの作成

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;
}
238
Jon Skeet

foreachループでToList()を呼び出します。この方法では、一時変数のコピーは必要ありません。 .Net 3.5以降で使用可能なLinqに依存します。

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

この行のコレクションを変更しています:

colStates [key] = 0;

そうすることで、基本的にその時点で何かを削除して再挿入します(IEnumerableに関する限りは)。

格納している値のmemberを編集する場合は問題ありませんが、値自体を編集しているため、IEnumberableはそれを好みません。

私が使用した解決策は、foreachループを削除し、forループを使用することです。単純なforループでは、コレクションに影響しないことがわかっている変更をチェックしません。

方法は次のとおりです。

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

ForEachでキーや値を直接変更することはできませんが、メンバーを変更することはできます。たとえば、これは動作するはずです:

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 } );
6
Jeremy Frey

辞書に対していくつかのlinqクエリを実行し、グラフをそれらの結果にバインドするのはどうですか?...

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);
}
3
Scott Ivey

あなたが創造的であると感じているなら、あなたはこのような何かをすることができます。辞書を逆方向にループして、変更を加えます。

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

}

確かに同一ではありませんが、とにかく興味があるかもしれません...

3
Hugoware

その場で変更するのではなく、古い辞書から新しい辞書を作成する必要があります。次のような(キー検索を使用するのではなく、KeyValuePair <、>を反復処理する)

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;
2
Richard

値さえもコレクションを変更することはできません。これらのケースを保存して、後で削除できます。次のようになります。

        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);
1
Samuel Carrijo

他の答えと一緒に、sortedDictionary.KeysまたはsortedDictionary.Valuesを取得し、それらをforeachでループすると、ソートされた順序でも実行されることに注意したいと思いました。これは、これらのメソッドが元の辞書の種類を保持するSystem.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionまたはSortedDictionary<TKey,TValue>.ValueCollectionオブジェクトを返すためです。

0
jep

dict.Valuesのリストコピーを作成してから、List.ForEachラムダ関数を反復に使用できます(または、以前に提案されたforeachループ)。

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

.NET 4.5以降では 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);

ただし、そのパフォーマンスは実際には単純な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>();
    }
}

結果:

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

免責事項:私はあまりC#をしません

HashTableに格納されているDictionaryEntryオブジェクトを変更しようとしています。 Hashtableには、1つのオブジェクト(DictionaryEntryのインスタンス)のみが格納されます。 HashTableを変更して列挙子を無効にするには、キーまたは値を変更するだけで十分です。

ループの外側でそれを行うことができます:

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

まず、変更する値のすべてのキーのリストを作成し、代わりにそのリストを反復処理します。

0
Cambium