La création d'une nouvelle liste pour modifier une collection dans chaque boucle est-elle un défaut de conception?

13

J'ai récemment rencontré cette opération invalide courante Collection was modifieden C #, et même si je la comprends parfaitement, cela semble être un problème si courant (google, environ 300 000 résultats!). Mais il semble également être une chose logique et simple de modifier une liste pendant que vous la parcourez.

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

Certaines personnes créeront une autre liste à parcourir, mais cela évite simplement le problème. Quelle est la vraie solution, ou quel est le véritable problème de conception ici? Nous semblons tous vouloir le faire, mais cela indique-t-il un défaut de conception?

tons31
la source
Vous pouvez utiliser un itérateur ou supprimer de la fin de la liste.
ElDuderino
@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . Ne manquez pas la You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementligne
SJuan76
En Java, il y a Iterator(fonctionne comme vous l'avez décrit) et ListIterator(fonctionne comme vous le souhaitez). Cela indiquerait que pour fournir des fonctionnalités d'itérateur sur un large éventail de types de collections et d'implémentations, il Iteratorest extrêmement restrictif (que se passe-t-il si vous supprimez un nœud dans une arborescence équilibrée? Cela a next()plus de sens), avec ListIteratorplus de puissance car moins cas d'utilisation.
SJuan76
@ SJuan76 dans d'autres langues, il existe encore plus de types d'itérateurs, par exemple C ++ a des itérateurs de transfert (équivalents à l'itérateur de Java), des itérateurs bidirectionnels (comme ListIterator), des itérateurs à accès aléatoire, des itérateurs d'entrée (comme un itérateur Java qui ne prend pas en charge les opérations facultatives ) et les itérateurs de sortie (c'est-à-dire en écriture seule, ce qui est plus utile qu'il n'y paraît).
Jules

Réponses:

9

La création d'une nouvelle liste pour modifier une collection dans chaque boucle est-elle un défaut de conception?

La réponse courte: non

En termes simples, vous produisez un comportement indéfini lorsque vous parcourez une collection et la modifiez en même temps. Pensez à supprimer l' nextélément dans une séquence. Que se passerait-il si MoveNext()on l'appelle?

Un énumérateur reste valide tant que la collection reste inchangée. Si des modifications sont apportées à la collection, telles que l'ajout, la modification ou la suppression d'éléments, l'énumérateur est irrémédiablement invalidé et son comportement n'est pas défini. L'énumérateur n'a pas un accès exclusif à la collection; par conséquent, l'énumération via une collection n'est pas intrinsèquement une procédure thread-safe.

Source: MSDN

Soit dit en passant, vous pouvez réduire votre RemoveAllBookssimplementreturn new List<Book>()

Et pour retirer un livre, je recommande de retourner une collection filtrée:

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

Une mise en œuvre possible en étagère ressemblerait à:

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
la source
Vous vous contredisez lorsque vous répondez oui, puis donnez un exemple qui crée également une nouvelle liste. A part ça, je suis d'accord.
Esben Skov Pedersen
1
@EsbenSkovPedersen vous avez raison: DI pensait à modifier comme un défaut ...
Thomas Junk
6

La création d'une nouvelle liste pour la modifier est une bonne solution au problème selon lequel les itérateurs existants de la liste ne peuvent pas continuer de manière fiable après la modification à moins qu'ils ne sachent comment la liste a changé.

Une autre solution consiste à effectuer la modification à l'aide de l'itérateur plutôt que via l'interface de liste elle-même. Cela ne fonctionne que s'il ne peut y avoir qu'un seul itérateur - cela ne résout pas le problème pour plusieurs threads itérant simultanément sur la liste, ce qui (tant que l'accès aux données périmées est acceptable) crée une nouvelle liste.

Une solution alternative que les implémenteurs de l'infrastructure auraient pu prendre consiste à faire en sorte que la liste suive tous ses itérateurs et les avertisse lorsque des changements se produisent. Cependant, les frais généraux de cette approche sont élevés - pour qu'elle fonctionne en général avec plusieurs threads, toutes les opérations de l'itérateur devraient verrouiller la liste (pour s'assurer qu'elle ne change pas pendant que l'opération est en cours), ce qui rendrait toute la liste les opérations sont lentes. Il y aurait également une surcharge de mémoire non triviale, de plus, il nécessite que le runtime prenne en charge les références logicielles, contrairement à la première version du CLR de l'IICRC.

Dans une perspective différente, la copie de la liste pour la modifier vous permet d'utiliser LINQ pour spécifier la modification, ce qui se traduit généralement par un code plus clair que l'itération directe sur la liste, IMO.

Jules
la source