La creazione di una nuova lista per modificare una collezione in un ciclo per ogni ciclo è un difetto?

13

Recentemente mi sono imbattuto in questa operazione non valida comune Collection was modified in C # e, mentre la capisco perfettamente, sembra che sia un problema comune (google, circa 300k risultati!). Ma sembra anche essere una cosa logica e diretta modificare un elenco mentre lo attraversi.

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

Alcune persone creeranno un altro elenco per iterare, ma questo è solo schivare il problema. Qual è la vera soluzione o qual è il vero problema di progettazione qui? Sembra che vogliamo per farlo, ma è indicativo di un difetto di progettazione?

    
posta user2410532 29.08.2015 - 06:10
fonte

2 risposte

9

Is creating a new List to modify a collection in a for each loop a design flaw?

La risposta breve: no

In parole semplici, produci un comportamento indefinito quando esegui un'iterazione attraverso una raccolta e la modifichi contemporaneamente. Pensa a eliminare l'elemento next in una sequenza. Cosa succederebbe se si chiamasse MoveNext() ?

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined. The enumerator does not have exclusive access to the collection; therefore, enumerating through a collection is intrinsically not a thread-safe procedure.

Fonte: MSDN

A proposito, potresti ridurre il RemoveAllBooks a semplicemente return new List<Book>()

E per rimuovere un libro, ti consiglio di restituire una raccolta filtrata:

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

Una possibile implementazione della shelf dovrebbe essere:

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);
    }
}
    
risposta data 29.08.2015 - 11:46
fonte
6

Creare un nuovo elenco per modificarlo è una buona soluzione al problema che gli iteratori esistenti dell'elenco non possono continuare in modo affidabile dopo la modifica, a meno che non sappiano come è cambiato l'elenco.

Un'altra soluzione consiste nel fare la modifica usando l'iteratore piuttosto che attraverso l'interfaccia di lista stessa. Funziona solo se può esistere un solo iteratore - non risolve il problema per più thread che iterano sull'elenco contemporaneamente, il che (purché l'accesso ai dati obsoleti sia accettabile) la creazione di un nuovo elenco lo fa.

Una soluzione alternativa che gli implementatori del framework potrebbero aver preso è quella di tenere traccia della lista di tutti i suoi iteratori e avvisarli quando si verificano cambiamenti. Tuttavia, i costi generali di questo approccio sono elevati, perché funzioni in generale con più thread, tutte le operazioni di iteratore dovrebbero bloccare l'elenco (per assicurarsi che non cambi durante l'operazione), che renderebbe tutto l'elenco operazioni lente. Ci sarebbe anche un overhead di memoria non banale, in più richiede il runtime per supportare i riferimenti software, e IIRC non ha la prima versione del CLR.

Da una prospettiva diversa, copiare l'elenco per modificarlo ti consente di usare LINQ per specificare la modifica, che generalmente genera un codice più chiaro rispetto a quello diretto sull'elenco, IMO.

    
risposta data 29.08.2015 - 12:16
fonte

Leggi altre domande sui tag