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.