4

次のコードを検討してください。

List<int> list = new List<int>();
IEnumerable<int> enumerable = list;
IEnumerator<int> enumerator = enumerable.GetEnumerator();
list.Add(1);
bool any = enumerator.MoveNext();

実行時に、最後の行は以下をスローします。

InvalidOperationException: コレクションが変更されました。列挙操作が実行されない場合があります。

IEnumerators変更時に「コレクションが変更されました」という例外をスローする必要があることIEnumerableは理解していますが、これは理解できません。

の最初の呼び出しで がIEnumeratorこの例外をスローするのはなぜですか? は最初に呼び出されるまでの状態を表していないため、 fromではなく最初から変更の追跡を開始できないのはなぜですか?MoveNext()IEnumeratorIEnumerableMoveNext()MoveNext()GetEnumerator()

4

3 に答える 3

8

おそらく、「基になるコレクションが変更された場合、列挙子は無効になる」という規則は、「MoveNext への最初の呼び出しの後に基になるコレクションが変更された場合、列挙子は無効になる」という規則よりも単純だからです。または、それが実装されている方法です。さらに、Enumerator が作成された時点での基になるコレクションの状態を Enumerator が表していると仮定するのは合理的であり、別の動作に依存するとバグの原因になる可能性があります。

于 2012-07-02T02:25:02.010 に答える
5

イテレータの簡単な要約が必要な気がします。

反復子 (C# の IEnumerator および IEnumerable) は、基になる表現を公開することなく、順序付けられた方法で構造体の要素にアクセスするために使用されます。その結果、次のような非常に一般的な関数を使用できるようになります。

void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
    foreach (V value in collection)
        actor(value);
}

//Or the more verbose way
void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
    using (var iterator = collection.GetEnumerator())
    {
        while (iterator.MoveNext())
            actor(iterator.Current);
    }
}

//Or if you need to support non-generic collections (ArrayList, Queue, BitArray, etc)
void Iterator<T, V> (T collection, Action<V> actor) where T : IEnumerable
{
    foreach (object value in collection)
        actor((V)value);
}

C# 仕様に見られるように、トレードオフがあります。

5.3.3.16 Foreach ステートメント

foreach ( expr の型識別子) 埋め込みステートメント

  • expr の先頭での v の明確な代入状態は、stmt の先頭での v の状態と同じです。

  • embedded-statement または stmt の終点への制御フロー転送での v の明確な代入状態は、expr の最後の v の状態と同じです。

これは単に、値が読み取り専用であることを意味します。なぜ読み取り専用なのですか? それは簡単です。は非常に高レベルのステートメントであるためforeach、繰り返し処理しているコンテナーについて何も想定することはできません。二分木を繰り返し処理していて、foreach ステートメント内で値をランダムに割り当てることにした場合はどうなるでしょうか。foreach読み取り専用アクセスを強制しなかった場合、バイナリ ツリーはツリーに退化します。データ構造全体が混乱します。

しかし、これはあなたの最初の質問ではありませんでした。最初の要素にアクセスする前にコレクションを変更していたため、エラーがスローされました。なんで?このために、ILSpyを使用して List クラスを掘り下げました。List クラスのスニペットを次に示します。

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
    private int _version;

    public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
    {
        private List<T> list;
        private int version;
        private int index;

        internal Enumerator(List<T> list)
        {
            this.list = list;
            this.version = list._version;
            this.index = 0;
        }

        /* All the implemented functions of IEnumerator<T> and IEnumerator will throw 
           a ThrowInvalidOperationException if (this.version != this.list._version) */
    }
}

列挙子は、親リストの「バージョン」と親リストへの参照で初期化されます。 すべての反復操作は、初期バージョンが参照リストの現在のバージョンと同等であることを確認するためにチェックします。それらが同期していない場合、反復子は無効になります。なぜBCLはこれを行うのですか? 実装者が列挙子のインデックスが 0 (新しい列挙子を表す) であるかどうかを確認しなかったのはなぜですか?0 である場合は、単純にバージョンを再同期します。わからない。チームが IEnumerable を実装するすべてのクラス間での適合を望んでおり、それをシンプルに保ちたいと考えていたという仮説しか立てられません。したがって、List の列挙子 (および他のほとんどの列挙子) は、要素が範囲内にある限り、要素を区別しません。

これが問題の根本原因です。この機能が絶対に必要な場合は、独自のイテレータを実装する必要があり、最終的に独自の List を実装する必要がある場合があります。私の意見では、BCL の流れに逆らうにはあまりにも多くの作業が必要です。

以下は、BCL チームがおそらく従ったイテレータを設計する際のGoFからの引用です。

トラバース中に集計を変更するのは危険です。集約に対して要素が追加または削除されると、要素に 2 回アクセスしたり、完全に欠落したりする可能性があります。簡単な解決策は、集約をコピーしてコピーをトラバースすることですが、一般的にはコストが高すぎます

BCL チームは、時空間の複雑さと人的資源の点で費用がかかりすぎると判断した可能性が高いです。そして、この哲学は C# 全体に見られます。おそらく、foreach 内の変数の変更を許可するにはコストがかかりすぎ、リストの列挙子がリスト内のどこにあるかを判別するにはコストがかかりすぎ、ユーザーを抱きしめるにはコストがかかりすぎます。イテレータのパワーと制約を理解できるように、十分に説明できたことを願っています。

参考

リストの「バージョン」を変更し、現在のすべての列挙子を無効にするものは何ですか?

  • インデクサーによる要素の変更
  • Add
  • AddRange
  • Clear
  • Insert
  • InsertRange
  • RemoveAll
  • RemoveAt
  • RemoveRange
  • Reverse
  • Sort
于 2012-07-02T15:48:58.320 に答える
1

これは、 にプライベートversionフィールドがあり、が呼び出されたList<T>ときにチェックされるためです。MoveNextこれで、 をMyList<T>実装するカスタムがあるかどうかがわかったので、 のIEnumerable<T>チェックを回避しversion、コレクションが変更されていても列挙を許可できます (ただし、予期しない動作が発生する可能性があります)。

于 2012-07-02T02:32:35.150 に答える