Criar uma nova lista para modificar uma coleção em um loop para cada loop é uma falha de design?


13

Recentemente, eu corri para essa operação inválida comum Collection was modifiedem C # e, embora eu a compreenda completamente, parece ser um problema tão comum (google, cerca de 300k resultados!). Mas também parece uma coisa lógica e direta modificar uma lista enquanto você a percorre.

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

Algumas pessoas criarão outra lista para percorrer, mas isso está apenas evitando o problema. Qual é a solução real ou qual é o problema de design real aqui? Todos nós parecemos querer fazer isso, mas isso é indicativo de uma falha de design?


Você pode usar um iterador ou remover do final da lista.
ElDuderino 29/08/2015

@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . Não perca a You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementlinha
SJuan76

Em Java existem Iterator(funciona como você descreveu) e ListIterator(funciona como você deseja). Isso indicaria que, para fornecer funcionalidade do iterador em uma ampla variedade de tipos e implementações de coleções, Iteratoré extremamente restritivo (o que acontece se você excluir um nó em uma árvore balanceada? Faz next()mais sentido), ListIteratorsendo mais poderoso por ter menos casos de uso.
precisa saber é o seguinte

@ SJuan76 em outros idiomas, existem ainda mais tipos de iteradores, por exemplo, o C ++ possui iteradores de encaminhamento (equivalentes ao iterador de Java), iteradores bidirecionais (como ListIterator), iteradores de acesso aleatório, iteradores de entrada (como um iterador de Java que não suporta operações opcionais ) e iteradores de saída (ou seja, somente gravação, o que é mais útil do que parece).
Jules

Respostas:


9

Criar uma nova lista para modificar uma coleção em um loop para cada loop é uma falha de design?

A resposta curta: não

Simplificando, você produz um comportamento indefinido quando itera uma coleção e a modifica ao mesmo tempo. Pense em excluir o nextelemento em uma sequência. O que aconteceria se MoveNext()fosse chamado?

Um enumerador permanece válido enquanto a coleção permanecer inalterada. Se forem feitas alterações na coleção, como adicionar, modificar ou excluir elementos, o enumerador é irrecuperavelmente invalidado e seu comportamento é indefinido. O enumerador não tem acesso exclusivo à coleção; portanto, enumerar por meio de uma coleção não é intrinsecamente um procedimento seguro para threads.

Fonte: MSDN

A propósito, você pode encurtar o seu RemoveAllBookspara simplesmentereturn new List<Book>()

E para remover um livro, recomendo retornar uma coleção filtrada:

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

Uma possível implementação em prateleira seria semelhante 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);
    }
}

Você se contradiz quando responde que sim e depois fornece um exemplo que também cria uma nova lista. Fora isso, eu concordo.
Esben Skov Pedersen

1
@EsbenSkovPedersen você está certo: DI estava pensando em modificar como uma falha ...
Thomas Junk

6

Criar uma nova lista para modificá-la é uma boa solução para o problema de que os iteradores existentes da lista não podem continuar com segurança após a alteração, a menos que eles saibam como a lista foi alterada.

Outra solução é fazer a modificação usando o iterador em vez da interface da lista. Isso funciona apenas se houver apenas um iterador - não resolve o problema de vários threads repetindo a lista simultaneamente, o que (desde que o acesso a dados antigos seja aceitável) criar uma nova lista.

Uma solução alternativa que os implementadores de estrutura poderiam ter adotado é fazer com que a lista rastreie todos os seus iteradores e notifique-os quando ocorrerem alterações. No entanto, as despesas gerais dessa abordagem são altas - para funcionar em geral com vários encadeamentos, todas as operações do iterador precisariam bloquear a lista (para garantir que ela não seja alterada enquanto a operação estiver em andamento), o que tornaria toda a lista operações lentas. Também haveria sobrecarga de memória não trivial, além de exigir o tempo de execução para oferecer suporte a referências flexíveis, e o IIRC, a primeira versão do CLR, não.

De uma perspectiva diferente, copiar a lista para modificá-la permite usar o LINQ para especificar a modificação, que geralmente resulta em um código mais claro do que na iteração direta sobre a lista, IMO.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.