La creazione di un nuovo elenco per modificare una raccolta in a per ogni ciclo è un difetto di progettazione?


13

Di recente mi sono imbattuto in questa operazione non valida comune Collection was modifiedin C #, e mentre la capisco pienamente, sembra essere un problema così comune (Google, circa 300k risultati!). Ma sembra anche essere una cosa logica e semplice 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 scorrere, ma questo sta solo schivando il problema. Qual è la vera soluzione o qual è il vero problema di progettazione qui? Sembriamo tutti voler farlo, ma è indicativo di un difetto di design?


È possibile utilizzare un iteratore o rimuovere dalla fine dell'elenco.
ElDuderino,

@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . Da non perdere la You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementlinea
SJuan76

In Java ci sono Iterator(funziona come hai descritto) e ListIterator(funziona come desideri). Ciò significherebbe che, al fine di fornire funzionalità iteratore su una vasta gamma di tipi e implementazioni di raccolte, Iteratorè estremamente restrittivo (cosa succede se si elimina un nodo in un albero bilanciato? Ha next()più senso), ListIteratoressendo più potente a causa della minore casi d'uso.
SJuan76,

@ SJuan76 in altre lingue ci sono ancora più tipi di iteratori, ad es. C ++ ha iteratori forward (equivalenti a Iterator di Java), iteratori bidirezionali (come ListIterator), iteratori ad accesso casuale, iteratori di input (come un Iterator Java che non supporta le operazioni opzionali ) e output iteratori (cioè di sola scrittura, che è più utile di quanto sembri).
Jules,

Risposte:


9

La creazione di un nuovo elenco per modificare una raccolta in a per ogni ciclo è un difetto di progettazione?

La risposta breve: no

In parole semplici, produci un comportamento indefinito quando esegui l'iterazione di una raccolta e la modifichi allo stesso tempo. Pensa di eliminare l' nextelemento in una sequenza. Cosa succederebbe se MoveNext()si chiamasse?

Un enumeratore rimane valido finché la raccolta rimane invariata. Se vengono apportate modifiche alla raccolta, come l'aggiunta, la modifica o l'eliminazione di elementi, l'enumeratore viene invalidato irrimediabilmente e il suo comportamento non è definito. L'enumeratore non ha accesso esclusivo alla raccolta; pertanto, l'enumerazione attraverso una raccolta non è intrinsecamente una procedura thread-safe.

Fonte: MSDN

A proposito, potresti accorciare il tuo RemoveAllBookssemplicementereturn 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 dello scaffale sarebbe simile a:

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

Ti contraddici quando rispondi sì e poi fornisci un esempio che crea anche un nuovo elenco. A parte questo, sono d'accordo.
Esben Skov Pedersen,

1
@EsbenSkovPedersen hai ragione: DI stava pensando di modificare come un difetto ...
Thomas Junk

6

La creazione di 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 è apportare la modifica utilizzando l'iteratore anziché tramite l'interfaccia dell'elenco stesso. Funziona solo se può esserci un solo iteratore: non risolve il problema per più thread che ripetono simultaneamente l'elenco, cosa che crea un nuovo elenco (purché sia ​​accettabile l'accesso a dati non aggiornati).

Una soluzione alternativa che gli implementatori del framework avrebbero potuto prendere è di far sì che l'elenco rintracci tutti i suoi iteratori e li informi quando si verificano cambiamenti. Tuttavia, i costi generali di questo approccio sono elevati: per funzionare in generale con più thread, tutte le operazioni dell'iteratore dovrebbero bloccare l'elenco (per assicurarsi che non cambi mentre l'operazione è in corso), il che renderebbe tutto l'elenco operazioni lente. Ci sarebbe anche un sovraccarico di memoria non banale, inoltre richiede il runtime per supportare i riferimenti software, e IIRC la prima versione del CLR no.

Da una prospettiva diversa, la copia dell'elenco per modificarlo consente di utilizzare LINQ per specificare la modifica, che generalmente porta a un codice più chiaro rispetto all'iterazione diretta dell'elenco, IMO.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.