Ist das Erstellen einer neuen Liste zum Ändern einer Sammlung in einer Schleife für jede Schleife ein Konstruktionsfehler?

13

Ich bin kürzlich auf diese häufig auftretende ungültige Operation Collection was modifiedin C # gestoßen, und obwohl ich sie vollständig verstehe, scheint es sich um ein so häufig auftretendes Problem zu handeln (Google, ca. 300.000 Ergebnisse!). Es scheint aber auch logisch und unkompliziert zu sein, eine Liste zu ändern, während Sie sie durchgehen.

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

Einige Leute werden eine andere Liste erstellen, um sie zu durchlaufen, aber dies umgeht nur das Problem. Was ist die wirkliche Lösung oder was ist das eigentliche Designproblem hier? Wir alle scheinen dies zu wollen , aber deutet dies auf einen Konstruktionsfehler hin?

tones31
quelle
Sie können einen Iterator verwenden oder vom Ende der Liste entfernen.
ElDuderino
@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . Verpassen Sie nicht die You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementLinie
SJuan76
In Java gibt es Iterator(funktioniert wie beschrieben) und ListIterator(funktioniert wie gewünscht). Dies würde darauf hinweisen, dass die Bereitstellung von Iteratorfunktionen für eine Vielzahl von Auflistungstypen und -implementierungen Iteratoräußerst restriktiv ist (was passiert, wenn Sie einen Knoten in einer ausgeglichenen Baumstruktur löschen? Ergibt keinen next()Sinn mehr) und ListIteratoraufgrund der geringeren Leistung leistungsfähiger ist Anwendungsfälle.
SJuan76
@ SJuan76 in anderen Sprachen gibt es noch mehr Iteratortypen, z. B. C ++ hat Forward-Iteratoren (entspricht Javas Iterator), bidirektionale Iteratoren (wie ListIterator), Direktzugriffs-Iteratoren, Eingabe-Iteratoren (wie ein Java-Iterator, der die optionalen Operationen nicht unterstützt) ) und Ausgabe-Iteratoren (dh nur schreiben, was nützlicher ist als es klingt).
Jules

Antworten:

9

Ist das Erstellen einer neuen Liste zum Ändern einer Sammlung in einer Schleife für jede Schleife ein Konstruktionsfehler?

Die kurze Antwort: nein

Einfach gesagt, erzeugen Sie undefiniertes Verhalten, wenn Sie eine Sammlung durchlaufen und gleichzeitig ändern. Denken Sie daran, das Element in einer Sequenz zu löschennext . Was würde passieren, wenn MoveNext()angerufen wird?

Ein Enumerator bleibt gültig, solange die Sammlung unverändert bleibt. Wenn Änderungen an der Auflistung vorgenommen werden, z. B. das Hinzufügen, Ändern oder Löschen von Elementen, wird der Enumerator unwiederbringlich ungültig und sein Verhalten ist nicht definiert. Der Enumerator hat keinen exklusiven Zugriff auf die Auflistung. Daher ist das Auflisten einer Auflistung an sich kein threadsicheres Verfahren.

Quelle: MSDN

Übrigens könntest du deine RemoveAllBookszu einfach kürzenreturn new List<Book>()

Und um ein Buch zu entfernen, empfehle ich, eine gefilterte Sammlung zurückzugeben:

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

Eine mögliche Regalimplementierung würde so aussehen:

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);
    }
}
Thomas Junk
quelle
Sie widersprechen sich, wenn Sie mit Ja antworten und dann ein Beispiel angeben, das auch eine neue Liste erstellt. Ansonsten stimme ich zu.
Esben Skov Pedersen
1
@EsbenSkovPedersen Sie haben Recht: DI dachte an Modifizieren als Fehler ...
Thomas Junk
6

Das Erstellen einer neuen Liste zum Ändern ist eine gute Lösung für das Problem, dass vorhandene Iteratoren der Liste nach der Änderung nur dann zuverlässig fortgesetzt werden können, wenn sie wissen, wie sich die Liste geändert hat.

Eine andere Lösung besteht darin, die Änderung mithilfe des Iterators und nicht über die Listenschnittstelle selbst vorzunehmen. Dies funktioniert nur, wenn es nur einen Iterator geben kann - es löst nicht das Problem, dass mehrere Threads gleichzeitig über die Liste iterieren, was beim Erstellen einer neuen Liste (solange auf veraltete Daten zugegriffen werden kann) der Fall ist.

Eine alternative Lösung, die die Framework-Implementierer hätten ergreifen können, besteht darin, die Liste alle ihre Iteratoren nachverfolgen zu lassen und sie zu benachrichtigen, wenn Änderungen auftreten. Der Aufwand für diesen Ansatz ist jedoch hoch. Damit er im Allgemeinen mit mehreren Threads funktioniert, müssen alle Iteratoroperationen die Liste sperren (um sicherzustellen, dass sie sich während der Ausführung der Operation nicht ändert), sodass alle Threads aufgelistet werden Operationen langsam. Es würde auch nicht-trivialen Speicher-Overhead geben, plus es erfordert die Laufzeit, um weiche Referenzen zu unterstützen, und IIRC, die erste Version der CLR, tat es nicht.

Wenn Sie die Liste aus einer anderen Perspektive kopieren, um sie zu ändern, können Sie LINQ verwenden, um die Änderung anzugeben. Dies führt im Allgemeinen zu einem klareren Code als das direkte Durchlaufen der Liste (IMO).

Jules
quelle