¿Es una falla de diseño crear una nueva Lista para modificar una colección en cada ciclo?

13

Recientemente me encontré con esta operación inválida común Collection was modifieden C #, y aunque lo entiendo completamente, parece ser un problema tan común (¡Google, alrededor de 300 mil resultados!). Pero también parece ser lógico y sencillo modificar una lista mientras la revisas.

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

Algunas personas crearán otra lista para iterar, pero esto solo está esquivando el problema. ¿Cuál es la solución real o cuál es el problema de diseño real aquí? Todos parecemos querer hacer esto, pero ¿es indicativo de una falla de diseño?

tonos31
fuente
Puede usar un iterador o eliminarlo al final de la lista.
ElDuderino
@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . No te pierdas la You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementlínea
SJuan76
En Java hay Iterator(funciona como lo describiste) y ListIterator(funciona como deseas). Esto indicaría que, para proporcionar funcionalidad de iterador en una amplia gama de tipos de colecciones e implementaciones, Iteratores extremadamente restrictivo (¿qué sucede si elimina un nodo en un árbol equilibrado? next()Ya tiene sentido), ListIteratorsiendo más poderoso debido a que tiene menos casos de uso
SJuan76
@ SJuan76 en otros idiomas todavía hay más tipos de iteradores, por ejemplo, C ++ tiene iteradores de reenvío (equivalentes al Iterator de Java), iteradores bidireccionales (como ListIterator), iteradores de acceso aleatorio, iteradores de entrada (como un Iterator de Java que no admite las operaciones opcionales ) y los iteradores de salida (es decir, solo escritura, que es más útil de lo que parece).
Jules

Respuestas:

9

¿Es una falla de diseño crear una nueva Lista para modificar una colección en cada ciclo?

La respuesta corta: no

En pocas palabras, produce un comportamiento indefinido cuando itera a través de una colección y la modifica al mismo tiempo. Piense en eliminar el nextelemento en una secuencia. Que pasaria siMoveNext() se llama?

Un enumerador sigue siendo válido mientras la colección permanezca sin cambios. Si se realizan cambios en la colección, como agregar, modificar o eliminar elementos, el enumerador se invalida irremediablemente y su comportamiento es indefinido. El enumerador no tiene acceso exclusivo a la colección; por lo tanto, enumerar a través de una colección no es intrínsecamente un procedimiento seguro para subprocesos.

Fuente: MSDN

Por cierto, podrías acortar tu RemoveAllBooks simplementereturn new List<Book>()

Y para eliminar un libro, recomiendo devolver una colección filtrada:

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

Una posible implementación de estantería se vería así:

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
fuente
Se contradice a sí mismo cuando responde que sí y luego proporciona un ejemplo que también crea una nueva lista. Aparte de eso, estoy de acuerdo.
Esben Skov Pedersen
1
@EsbenSkovPedersen tiene razón: DI estaba pensando en modificarlo como un defecto ...
Thomas Junk
6

Crear una nueva lista para modificarla es una buena solución al problema de que los iteradores existentes de la lista no pueden continuar de manera confiable después del cambio a menos que sepan cómo ha cambiado la lista.

Otra solución es realizar la modificación utilizando el iterador en lugar de hacerlo a través de la interfaz de la lista. Esto solo funciona si solo puede haber un iterador; no resuelve el problema de varios hilos que iteran sobre la lista simultáneamente, lo que (siempre y cuando el acceso a datos obsoletos sea aceptable) crea una nueva lista.

Una solución alternativa que los implementadores del marco podrían haber tomado es hacer que la lista rastree todos sus iteradores y les notifique cuando ocurran cambios. Sin embargo, los gastos generales de este enfoque son altos: para que funcione en general con varios subprocesos, todas las operaciones de iterador necesitarían bloquear la lista (para asegurarse de que no cambie mientras la operación está en progreso), lo que haría que toda la lista operaciones lentas. También habría una sobrecarga de memoria no trivial, además de que requiere el tiempo de ejecución para admitir referencias suaves, y IIRC la primera versión del CLR no.

Desde una perspectiva diferente, copiar la lista para modificarla le permite usar LINQ para especificar la modificación, lo que generalmente da como resultado un código más claro que iterar directamente sobre la lista, IMO.

Jules
fuente