¿Está creando una nueva Lista para modificar una colección en un bucle para cada bucle un defecto de diseño?

13

Recientemente me encontré con esta operación no válida común Collection was modified en C #, y aunque lo entiendo completamente, parece ser un problema tan común (¡google, aproximadamente 300k resultados!). Pero también parece ser algo lógico y sencillo modificar una lista mientras la recorres.

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 simplemente está esquivando el problema. ¿Cuál es la solución real, o cuál es el problema de diseño real aquí? Parece que todos queremos hacer esto, pero ¿es indicativo de una falla de diseño?

    
pregunta user2410532 29.08.2015 - 06:10

2 respuestas

9
  

¿La creación de una nueva Lista para modificar una colección en un bucle para cada bucle es un defecto de diseño?

La respuesta corta: no

Hablando simplemente, usted produce un comportamiento indefinido , cuando recorre una colección y la modifica al mismo tiempo. Piense en eliminar el elemento next en una secuencia. ¿Qué pasaría si se llama a MoveNext() ?

  

Un enumerador permanece 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 de forma irrecuperable y su comportamiento no está definido.   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 a simplemente return 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 en estante serí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);
    }
}
    
respondido por el Thomas Junk 29.08.2015 - 11:46
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 para múltiples subprocesos que se repiten en la lista de forma concurrente, lo que (siempre que sea aceptable acceder a datos obsoletos), crear una nueva lista sí lo hace.

Una solución alternativa que los implementadores del marco podrían haber tomado es hacer que la lista rastree a todos sus iteradores y notificarles cuando ocurran cambios. Sin embargo, los costos generales de este enfoque son altos: para que funcione en general con varios subprocesos, todas las operaciones del iterador deberán bloquear la lista (para asegurarse de que no cambie mientras la operación está en curso), 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 blandas, y IIRC no lo hizo con la primera versión del CLR.

Desde una perspectiva diferente, copiar la lista para modificarla le permite usar LINQ para especificar la modificación, lo que generalmente resulta en un código más claro que la iteración directa sobre la lista, IMO.

    
respondido por el Jules 29.08.2015 - 12:16

Lea otras preguntas en las etiquetas