29
class Program
{
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<string, int>()
        {
            {"1", 1}, {"2", 2}, {"3", 3}
        };

        foreach (var s in dictionary.Keys)
        {
            // Throws the "Collection was modified exception..." on the next iteration
            // What's up with that?

            dictionary[s] = 1;  
        }
    }
}

リストを列挙するときにこの例外がスローされる理由を完全に理解しています。列挙中に、列挙されたオブジェクトの構造が変更されないことを期待するのは合理的と思われます。ただし、辞書の値を変更すると、その構造も変更されますか?具体的には、そのキーの構造は?

4

9 に答える 9

20

値とキーがペアで保存されるためです。キーと値の個別の構造ではなく、両方をペア値のセットとして格納する単一の構造があります。値を変更する場合は、キーと値の両方を含む単一の基礎となる構造を変更する必要があります。

値を変更すると、基礎となる構造の順序が必然的に変更されますか?いいえ。ただし、これは実装固有の詳細であり、Dictionary<TKey,TValue>クラスは、APIの一部として値の変更を許可することにより、これを明らかにしないと正しく見なされます。

于 2009-10-13T20:30:27.970 に答える
8

実際、あなたがどこから来たのかわかりました。ここでの回答のほとんどが気付かないのは、ディクショナリのアイテム自体ではなく、キーのリストを反復処理していることです。.NET Framework プログラマーが望むのであれば、ディクショナリの構造に加えられた変更とディクショナリ内の値に加えられた変更をかなり簡単に区別することができました。それにもかかわらず、人々がコレクションのキーを反復するときでさえ、通常はとにかく値を取得することになります。.NET フレームワークの設計者は、これらの値を反復処理している場合、List の場合と同様に、何かによって値が変更されているかどうかを知りたいと考えたのではないでしょうか。それか、彼らがしなかったかのどちらかです」

于 2009-10-13T20:55:16.050 に答える
8

Vitaliy のおかげで、私は戻ってコードをもう少し調べましたが、これを許可しないのは特定の実装上の決定のようです (以下のスニペットを参照)。ディクショナリは、既存のアイテムの値を変更するときにインクリメントされる、バージョンと呼ばれるプライベート値を保持します。列挙子が作成されると、その時点での値が記録され、MoveNext への各呼び出しがチェックされます。

for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
{
    if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
    {
        if (add)
        {
            ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
        }
        this.entries[i].value = value;
        this.version++;
        return;
    }
}

これが必要な理由がわかりません。値のプロパティを自由に変更できますが、新しい値に割り当てることはできません。

public class IntWrapper
{
  public IntWrapper(int v) { Value = v; }
  public int Value { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    var kvp = new KeyValuePair<string, int>("1",1);
    kvp.Value = 17;
    var dictionary = new Dictionary<string, IntWrapper>(){
      {"1", new IntWrapper(1)}, 
      {"2", new IntWrapper(2)}, 
      {"3", new IntWrapper(3)} };

    foreach (var s in dictionary.Keys)
    {
      dictionary[s].Value = 1;  //OK
      dictionary[s] = new IntWrapper(1); // boom
    }
  } 
}
于 2009-10-13T20:57:08.003 に答える
5

辞書に新しいキーを挿入したばかりである可能性がありますが、実際には変更されdictionary.Keysます。この特定のループでは決して発生しませんが、[]操作は一般にキーのリストを変更できるため、これはミューテーションとしてフラグが立てられます。

于 2009-10-13T20:31:36.020 に答える
5

インデクサーのオンDictionaryは、コレクションの構造を変更する可能性のある操作です。これは、そのようなキーを持つ新しいエントリがまだ存在しない場合に追加するためです。これは明らかにここでは当てはまりませんがDictionary、オブジェクトに対するすべての操作が「変更」と「非変更」に分割され、すべての「変更」操作が列挙子を無効にするという点で、コントラクトは意図的に単純に保たれていると思います。実際には何も変更しないでください。

于 2009-10-13T20:32:43.530 に答える
1

この問題を回避する方法に興味がある人のために、動作する Vitaliy のコードの修正版を次に示します。

class Program
{
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<string, int>()
        {
            {"1", 1}, {"2", 2}, {"3", 3}
        };

        string[] keyArray = new string[dictionary.Keys.Count];
        dictionary.Keys.CopyTo(keyArray, 0);
        foreach (var s in keyArray)
        {
            dictionary[s] = 1;
        }
    }
}

答えは、キーを別の列挙型にコピーしてから、そのコレクションを反復処理することです。何らかの理由で、物事を簡単にする KeyCollection.ToList メソッドがありません。代わりに、キーを配列にコピーする KeyCollection.CopyTo メソッドを使用する必要があります。

于 2013-03-25T23:35:44.913 に答える
1

ドキュメントから (Dictionary.Item プロパティ):

Dictionary に存在しないキーの値を設定することにより、Item プロパティを使用して新しい要素を追加することもできます。プロパティ値を設定すると、キーがディクショナリにある場合、そのキーに関連付けられた値が割り当てられた値に置き換えられます。キーがディクショナリにない場合、キーと値がディクショナリに追加されます。対照的に、Add メソッドは既存の要素を変更しません。

したがって、ジョンが指摘しているように、リストの内容が変更されていないことをフレームワークが認識する方法はありません。

于 2009-10-13T20:35:43.883 に答える
0

簡単に言えば、実際にはキーを変更していなくても、ディクショナリ コレクションを変更しているということです。したがって、更新後にコレクションにアクセスする次の反復では、最後のアクセス以降にコレクションが変更されたことを示す例外がスローされます (当然のことです)。

必要なことを行うには、要素を変更しても反復子例外がトリガーされないように、要素を反復する別の方法が必要です。

于 2009-10-13T20:58:58.530 に答える
0

これは、複数のスレッドでコレクションを反復処理できるように .Net を設計したためです。したがって、イテレータをマルチスレッド化できるようにするか、それを防ぎ、反復中にコレクションを変更できるようにする必要があります。これには、オブジェクトを単一のスレッドで反復するように制限する必要があります。両方を持つことはできません。

実際、あなたの質問に対する答えは、入力したコードが実際にコンパイラーによって生成された ([CompilerGenerated]) ステート マシンになり、イテレーターがコレクションの状態を維持して、yield マジックを提供できるようになるということです。そのため、コレクションを同期せずに、あるスレッドで反復処理を行い、別のスレッドで操作すると、おかしなことが起こります。

チェックアウト: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

また、http: //docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html 「イテレータは、一度に 1 つのスレッドだけが使用するように設計されています。」

于 2014-06-26T18:53:12.887 に答える