forループ内のコレクションを変更するための新しいリストを作成することは設計上の欠陥ですか?


13

私は最近Collection was modified、C#でこの一般的な無効な操作に出くわしましたが、それを完全に理解している間、それはそのような一般的な問題のようです(グーグル、約30万件の結果!)。しかし、リストを確認しながら変更することも論理的で簡単なことのようです。

List<Book> myBooks = new List<Book>();

public void RemoveAllBooks(){
    foreach(Book book in myBooks){
         RemoveBook(book);
    }
}

RemoveBook(Book book){
    if(myBooks.Contains(book)){
         myBooks.Remove(book);
         if(OnBookEvent != null)
            OnBookEvent(this, new EventArgs("Removed"));
    }
}

繰り返し処理するために別のリストを作成する人もいますが、これは問題を回避しているだけです。実際の解決策は何ですか、または実際の設計上の問題は何ですか?私たちは皆これをやりたいようですが、それは設計上の欠陥を示していますか?


イテレータを使用するか、リストの最後から削除できます。
エルデュデリノ

@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx。お見逃しなくYou consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementライン
SJuan76

Javaには、Iterator(説明したとおりに動作する)とListIterator(必要に応じて動作する)があります。これは、コレクションの種類と実装の広い範囲でイテレータの機能を提供するために、これを指すことになりIterator(あなたはバランスの取れたツリー?ないのでノードを削除した場合、何が起こるかは極めて限定的であるnext()と、もはやメイクセンス)ListIterator以下有することに起因する、より強力なものユースケース。
SJuan76

他の言語の@ SJuan76には、さらに多くのイテレータタイプがあります。たとえば、C ++にはフォワードイテレータ(Javaのイテレータと同等)、双方向イテレータ(ListIteratorなど)、ランダムアクセスイテレータ、入力イテレータ(オプション操作をサポートしないJavaイテレータなど) )および出力イテレータ(つまり、書き込み専用。見た目よりも便利です)。
ジュール

回答:


9

forループ内のコレクションを変更するための新しいリストを作成することは設計上の欠陥ですか?

短い答え:いいえ

簡単に言えば、コレクションを反復処理し、同時に変更すると、未定義の動作が発生します。シーケンス内の要素を削除することを考えてくださいnextMoveNext()が呼び出されたらどうなりますか?

コレクションが変更されない限り、列挙子は有効なままです。要素の追加、変更、削除などの変更がコレクションに加えられた場合、列挙子は回復不能に無効になり、その動作は未定義になります。列挙子にはコレクションへの排他的アクセス権はありません。したがって、コレクションを介した列挙は本質的にスレッドセーフな手順ではありません。

ソース:MSDN

ところで、RemoveAllBooks単純にreturn new List<Book>()

また、本を削除するには、フィルターされたコレクションを返すことをお勧めします。

return books.Where(x => x.Author != "Bob").ToList();

可能なシェルフ実装は次のようになります。

public class Shelf
{
    List<Book> books=new List<Book> {
        new Book ("Paul"),
        new Book ("Peter")
    };

    public IEnumerable<Book> getAllBooks(){
        foreach(Book b in books){
            yield return b;
        }
    }

    public void RemovePetersBooks(){
        books= books.Where(x=>x.Author!="Peter").ToList();
    }

    public void EmptyShelf(){
        books = new List<Book> ();
    }

    public Shelf ()
    {
    }
}

public static void Main (string[] args)
{
    Shelf s = new Shelf ();
    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
    s.RemovePetersBooks ();

    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
    s.EmptyShelf ();
    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
}

「はい」と答えてから、新しいリストも作成する例を提供すると、自分は矛盾します。それ以外は同意します。
エスベンスコフペダーセン

1
DIは...欠陥として修正することを考えていた:あなたが正しいしている@EsbenSkovPedersen
トーマス・ジャンク

6

新しいリストを作成して変更することは、リストの既存のイテレータがリストの変更方法を知らない限り、変更後に確実に続行できないという問題に対する優れたソリューションです。

別の解決策は、リストインターフェイス自体ではなくイテレータを使用して変更を加えることです。これは、イテレータが1つしか存在しない場合にのみ機能します。複数のスレッドがリストを同時に反復する問題を解決しません(古いデータへのアクセスが許容される限り)新しいリストを作成します。

フレームワークの実装者がとることができた代替ソリューションは、リストにすべてのイテレータを追跡させ、変更が発生したときにそれらを通知することです。ただし、このアプローチのオーバーヘッドは高く、一般に複数のスレッドで機能するには、すべてのイテレータ操作がリストをロックする必要があり(操作の進行中にリストが変更されないようにするため)、すべてのリストが作成されます操作が遅い。また、重要なメモリオーバーヘッドも発生します。さらに、ソフト参照をサポートするためにランタイムが必要になり、CLRの最初のバージョンではサポートしていませんでした。

別の観点から、リストをコピーして変更すると、LINQを使用して変更を指定できるため、通常、リストIMOを直接反復するよりもコードが明確になります。

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