2

.netコレクションタイプ(または少なくとも一部のコレクションタイプ)では、コレクションを反復処理するときにコレクションを変更できないことを知っています。

たとえば、Listクラスには、次のようなコードがあります。

if (this.version != this.list._version)
 ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);

しかし、明らかにこれはイテレータクラスを設計する開発者の決定です。なぜならIEnumerable、下にあるコレクションが変更されたときに少なくとも例外をスローしない実装をいくつか提供できるからです。

それから私はいくつかの質問があります:

  • コレクションを反復処理しているときにコレクションを変更しないのはなぜですか?

  • 他の問題を発生させることなく、コレクションを反復処理するときに変更をサポートするコレクションを作成することは可能ですか?(注:最初の答えはこれにも答えることができます)

  • C#コンパイラが生成するとき、Enumeratorインターフェイスの実装はこのようなことを考慮に入れますか?

4

5 に答える 5

5

コレクションを反復処理しているときにコレクションを変更しないのはなぜですか?

一部のコレクションは反復時に変更できるため、グローバルに悪くはありません。ほとんどの場合、基になるコレクションが変更された場合でも正しく機能する効果的なイテレーターを作成することは非常に困難です。多くの場合、例外はイテレータのライターがパントして、それに対処したくないと言っていることです。

場合によっては、基になるコレクションが変更されたときにイテレータが何をすべきかが明確ではありません。明確なケースもありますが、他のケースでは、異なる人々が異なる行動を期待します。そのような状況にあるときはいつでも、より深い問題があることを示しています(繰り返しているシーケンスを変更してはいけません)。

他の問題を発生させることなく、コレクションを反復処理するときに変更をサポートするコレクションを作成することは可能ですか?(注:最初の答えはこれにも答えることができます)

もちろん。

リストについては、このイテレータを検討してください。

public static IEnumerable<T> IterateWhileMutating<T>(this IList<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

基になるリストから現在のインデックスまたはそれ以前のアイテムを削除すると、反復中にアイテムがスキップされます。現在のインデックスまたはその前にアイテムを追加すると、アイテムが複製されます。ただし、反復中に現在のインデックスを超えてアイテムを追加/削除した場合、問題は発生しません。気を付けて、リストからアイテムが削除/追加されたかどうかを確認し、それに応じてインデックスを調整することもできますが、常に機能するとは限らないため、すべてのケースを処理することはできません。のようなものがある場合はObservableCollection、追加/削除とそのインデックスの通知を受け取り、それに応じてインデックスを調整します。これにより、イテレータが基になるコレクションの変更を処理できるようになります(別のスレッドにない場合)。

缶のイテレータは、ObservableCollectionアイテムがいつ追加/削除されたか、そしてそれらがどこにあるかを知ることができるので、それに応じてその位置を調整することができます。組み込みのイテレータがミューテーションを適切に処理するかどうかはわかりませんが、基になるコレクションのミューテーションを処理するイテレータは次のとおりです。

public static IEnumerable<T> IterateWhileMutating<T>(
    this ObservableCollection<T> list)
{
    int i = 0;
    NotifyCollectionChangedEventHandler handler = (_, args) =>
    {
        switch (args.Action)
        {
            case NotifyCollectionChangedAction.Add:
                if (args.NewStartingIndex <= i)
                    i++;
                break;
            case NotifyCollectionChangedAction.Move:
                if (args.NewStartingIndex <= i)
                    i++;
                if (args.OldStartingIndex <= i) //note *not* else if
                    i--;
                break;
            case NotifyCollectionChangedAction.Remove:
                if (args.OldStartingIndex <= i)
                    i--;
                break;
            case NotifyCollectionChangedAction.Reset:
                i = int.MaxValue;//end the sequence
                break;
            default:
                //do nothing
                break;
        }
    };
    try
    {
        list.CollectionChanged += handler;
        for (i = 0; i < list.Count; i++)
        {
            yield return list[i];
        }
    }
    finally
    {
        list.CollectionChanged -= handler;
    }
}
  • シーケンスの「前」からアイテムが削除された場合、アイテムをスキップせずに通常どおり続行します。

  • アイテムがシーケンスの「前」に追加された場合、そのアイテムは表示されませんが、他のアイテムも2回表示されません。

  • アイテムが現在の位置の前から後に移動された場合、そのアイテムは2回表示されますが、他のアイテムがスキップまたは繰り返されることはありません。アイテムが現在の位置の後から現在の位置の前に移動された場合、そのアイテムは表示されませんが、それだけです。コレクションの後半から別の場所にアイテムを移動しても問題はなく、結果に移動が表示されます。前の場所から別の前の場所に移動すると、すべて問題なく移動します。イテレータによって「表示」されることはありません。

  • アイテムの交換は問題ではありません。ただし、現在の位置の「後」にある場合にのみ表示されます。

  • コレクションをリセットすると、シーケンスは現在の位置で正常に終了します。

このイテレータは、複数のスレッドがある状況を処理しないことに注意してください。別のスレッドが反復しているときに別のスレッドがコレクションを変更すると、悪いことが起こる可能性があります(アイテムがスキップまたは繰り返されたり、インデックスの範囲外の例外などの例外が発生したりすることもあります)。これにより、スレッドが1つしかない、またはイテレーターを移動したりコレクションを変更したりするコードを1つのスレッドだけが実行している、反復中の変更が可能になります。

C#コンパイラが列挙子インターフェイスを生成するとき、実装はこのようなことを考慮に入れますか?

コンパイラーはインターフェース実装を生成しません。人はそうします。

于 2013-01-29T17:58:57.937 に答える
4

コレクションの反復中にコレクションの変更を許可しない大きな理由の1つは、コレクション内の要素が削除された場合、または新しい要素が挿入された場合に、反復が破棄されることです。(コレクション内で反復が機能している場所に要素が挿入または削除されました。次の要素は何ですか?新しい停止条件は何ですか?)

于 2013-01-29T17:07:24.317 に答える
1

1つの理由はスレッドセーフです。List<T>別のスレッドがリストに追加されている場合、イテレータがのバッキング配列から正しい方法で読み取っていることを保証する方法はありません。これにより、新しい配列への再割り当てが発生する可能性があります。

List<T>ループを使用して列挙することでさえ、forこのスレッドセーフの欠如を示すことに注意する価値があります。

彼がクラスを作成するJaredParによるこのブログ投稿からThreadSafeList<T>

コレクションはIEnumerableを実装しなくなりました。IEnumerableは、コレクションが内部で変更されていない場合にのみ有効です。この方法で構築されたコレクションでこの保証を簡単に行う方法はないため、削除されました。

IEnumerable列挙中の変更を禁止するすべての実装があるわけではないことに言及する価値があります。並行コレクションはスレッドセーフな保証を行うため、そうします。

于 2013-01-29T17:07:02.693 に答える
0

変更する要素をロードするためにyieldステートメントを使用し、事後に実行します

反復中にコレクションを変更する必要がある場合(インデックスを作成できる場合)、forループを使用し、オブジェクトとループ宣言の関連付けを解除します...ただし、ループの周りでlockステートメントを使用して、オブジェクトを操作するのは1つだけです...そして、ループの次のパスで自分の操作を覚えておいてください...

于 2013-01-29T17:20:55.220 に答える
0

おそらくそれは可能ですが、IEnumerableおよびIEnumeratorインターフェースの意図外では、予期しない動作になります。

IEnumerable.GetEnumerator

コレクションが変更されない限り、列挙子は有効なままです。要素の追加、変更、削除など、コレクションに変更が加えられた場合、列挙子は回復不能に無効になり、その動作は未定義になります。

これにより、たとえばLinkedListなどのコレクションの問題を回避できます。4つのノードを持つリンクリストがあり、2番目のノードまで反復するとします。次に、リンクリストが変更され、2番目のノードがリンクリストの先頭に移動され、3番目のノードが末尾に移動されます。その時点で列挙子を次に使用することはどういう意味ですか?考えられる動作はあいまいで、簡単に推測することはできません。インターフェースを介してオブジェクトを処理する場合、基礎となるクラスが何であるか、およびそのクラスとその列挙子が変更に耐性があるかどうかを考慮する必要はありません。インターフェイスは、変更によって列挙子が無効になると言っているので、それが動作するはずです。

于 2013-01-29T17:32:05.737 に答える