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


191

辞書から円グラフを作成しようとしています。円グラフを表示する前に、データを整理します。パイの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);

回答:


259

辞書に値を設定すると、内部の「バージョン番号」が更新されます。これにより、イテレーター、およびキーまたは値のコレクションに関連付けられているイテレーターが無効になります。

私はあなたの要点を理解していますが、同時に、値のコレクションが反復の途中で変更される可能性がある場合は奇妙です-簡単にするために、バージョン番号は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;
}

24
私はこれが古いですけど、.NET 3.5を使用している場合(またはそれが4.0である?)次のように使用し、虐待のLINQすることができます:foreachの(colStates.Keys.ToList内の文字列のキーを()){...}
Machtyn

6
@Machtyn:もちろんですが、問題は特に.NET 2.0に関するものでした。そうでなければ、確かにLINQを使用したでしょう
Jon Skeet、2015年

「バージョン番号」は、ディクショナリの可視状態の一部ですか、それとも実装の詳細ですか?
匿名の臆病者

@SEinfringescopyright:直接表示されません。辞書を更新するとイテレータ無効になるという事実目に見えますが。
Jon Skeet

以下からのMicrosoftドキュメントの.NETフレームワーク4.8foreachのステートメントは、それだけには書いていない、コレクションからの読み取りができます列挙子のラッパー、です。したがって、これは実装の詳細であり、将来のバージョンで変更される可能性があると言えます。そして目に見えるのは、列挙子のユーザーがその契約に違反したことです。しかし、私は間違っているでしょう... Dictionaryがシリアル化されている場合、それは実際に表示されます。
匿名の臆病者

81

呼び出しToList()foreachループ。この方法では、一時変数のコピーは必要ありません。.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;
    }
}

とても良い改善です!
SpeziFish 2014年

1
それは使用する方が良いだろうforeach(var pair in colStates.ToList())キーへのアクセスを避けるためににコールする必要がなくなり価値をcolStates[key]...
user2864740

21

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

colStates [key] = 0;

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

保存している値のメンバーを編集する場合は問題ありませんが、値自体を編集しているため、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;    
    }
}

forループを使用してこの問題が発生します。dictionary [index] [key] = "abc"、ただし初期値 "xyz"に戻ります
Nick Chan Abdullah

1
このコードの修正はforループではなく、キーのリストをコピーしています。(foreachループに変換した場合でも機能します。)forループを使用して解決することはcolStates.Keys、の代わりにを使用することを意味しkeysます。
idbrii

6

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

3

辞書に対して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);
}

Linqは3.5でしか利用できませんか?.net 2.0を使用しています。
Aheho 2009

System.Core.DLLの3.5バージョンへの参照を使用して、2.0からそれを使用できます。それが実行したくないものである場合は、お知らせください。この回答は削除します。
スコットアイビー

1
私はおそらくこのルートに行かないでしょうが、それでもそれは良い提案です。同じ問題を抱えている誰かがつまずいた場合に備えて、答えはそのままにしておくことをお勧めします。
Aheho 2009

3

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

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

}

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


2

その場で変更するのではなく、古い辞書から新しい辞書を作成する必要があります。キールックアップを使用するのではなく、(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;

1

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

1

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

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

あなたはできるコレクションを変更します。変更されたコレクションに対してイテレータを使い続けることはできません
user2864740 2017年

0

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

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

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

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

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


0

のリストのコピーを作成して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

他の回答と一緒に、sortedDictionary.KeysまたはsortedDictionary.Valuesを使用してそれらをループするとforeach、並べ替えられた順序で処理されることに注意しました。これは、元のディクショナリの種類を維持するメソッドSystem.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionまたはSortedDictionary<TKey,TValue>.ValueCollectionオブジェクトが返されるため です。


0

この回答は2つのソリューションを比較するためのものであり、推奨されるソリューションではありません。

他の回答が提案するように別のリストを作成する代わりに、ループ停止条件のfor辞書Countを使用してループを使用しKeys.ElementAt(i)、キーを取得できます。

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

初心者のときは、キーリストを作成する必要がないため、これがより効率的だと思いました。テストを実行した後、forループソリューションの効率がはるかに低いことがわかりました。その理由はElementAtdictionary.KeysプロパティのO(n)であるため、コレクションの最初からn番目のアイテムに到達するまで検索を行うためです。

テスト:

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

結果:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.