¿Cómo eliminar elementos de una lista genérica mientras se itera sobre ella?

Estoy buscando un mejor patrón para trabajar con una lista de elementos que cada uno necesita procesar y luego, según el resultado, se eliminan de la lista.

No se puede usar .Remove(element) dentro de un foreach (var element in X) (porque da como resultado que Collection was modified; enumeration operation may not execute. Excepción) … tampoco se puede usar for (int i = 0; i < elements.Count(); i++) y .RemoveAt(i) porque interrumpe su posición actual en la colección relativa a i .

¿Hay una manera elegante de hacer esto?

Itere su lista en reversa con un ciclo for:

 for (int i = safePendingList.Count - 1; i >= 0; i--) { // some code // safePendingList.RemoveAt(i); } 

Ejemplo:

 var list = new List(Enumerable.Range(1, 10)); for (int i = list.Count - 1; i >= 0; i--) { if (list[i] > 5) list.RemoveAt(i); } list.ForEach(i => Console.WriteLine(i)); 

Alternativamente, puede usar el método RemoveAll con un predicado para probar en contra:

 safePendingList.RemoveAll(item => item.Value == someValue); 

Aquí hay un ejemplo simplificado para demostrar:

 var list = new List(Enumerable.Range(1, 10)); Console.WriteLine("Before:"); list.ForEach(i => Console.WriteLine(i)); list.RemoveAll(i => i > 5); Console.WriteLine("After:"); list.ForEach(i => Console.WriteLine(i)); 

Una solución simple y directa:

Utilice un for-loop estándar que se ejecute hacia atrás en su colección y RemoveAt(i) para eliminar elementos.

La iteración inversa debería ser lo primero que se debe tener en cuenta cuando quiera eliminar elementos de una Colección mientras itera sobre ella.

Afortunadamente, hay una solución más elegante que escribir un ciclo for que implica una escritura innecesaria y puede ser propenso a errores.

 ICollection test = new List(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); foreach (int myInt in test.Reverse()) { if (myInt % 2 == 0) { test.Remove(myInt); } } 
  foreach (var item in list.ToList()) { list.Remove(item); } 

Si agrega “.ToList ()” a su lista (o los resultados de una consulta LINQ), puede eliminar “elemento” directamente de “lista” sin la temida ” Se modificó la recostackción, la operación de enumeración puede no ejecutarse “. error. El comstackdor hace una copia de “lista”, para que pueda hacer la eliminación de manera segura en la matriz.

Si bien este patrón no es muy eficiente, tiene una sensación natural y es lo suficientemente flexible para casi cualquier situación . Por ejemplo, cuando quiere guardar cada “elemento” en un DB y eliminarlo de la lista solo cuando el guardado de DB tiene éxito.

Usar ToArray () en una lista genérica le permite hacer un Remove (elemento) en su Lista genérica:

  List strings = new List() { "a", "b", "c", "d" }; foreach (string s in strings.ToArray()) { if (s == "b") strings.Remove(s); } 

Seleccione los elementos que desea en lugar de tratar de eliminar los elementos que no desea. Esto es mucho más fácil (y generalmente también más eficiente) que eliminar elementos.

 var newSequence = (from el in list where el.Something || el.AnotherThing < 0 select el); 

Quería publicar esto como un comentario en respuesta al comentario dejado por Michael Dillon a continuación, pero es demasiado largo y probablemente sea útil tenerlo en mi respuesta de todos modos:

Personalmente, nunca eliminaría los elementos uno por uno, si es necesario eliminarlos, y luego llamar a RemoveAll que toma un predicado y solo reorganiza el arreglo interno una vez, mientras que Remove realiza una operación Array.Copy para cada elemento que elimine. RemoveAll es mucho más eficiente.

Y cuando está retrocediendo sobre una lista, ya tiene el índice del elemento que desea eliminar, por lo que sería mucho más eficiente llamar a RemoveAt , porque Remove primero hace un recorrido de la lista para encontrar el índice de la elemento que está tratando de eliminar, pero ya conoce ese índice.

Así que, en general, no veo ningún motivo para llamar a Remove en un ciclo for-loop. E idealmente, si es posible, utilice el código anterior para transmitir elementos de la lista según sea necesario, de modo que no se tenga que crear ninguna segunda estructura de datos.

Usando .ToList () hará una copia de su lista, como se explica en esta pregunta: ToList () – ¿Crea una nueva lista?

Al usar ToList (), puede eliminar de su lista original, porque en realidad está iterando sobre una copia.

 foreach (var item in listTracked.ToList()) { if (DetermineIfRequiresRemoval(item)) { listTracked.Remove(item) } } 

Como cualquier eliminación se toma con una condición que puede usar

 list.RemoveAll(item => item.Value == someValue); 

Si la función que determina qué elementos eliminar no tiene efectos secundarios y no muta el elemento (es una función pura), una solución simple y eficiente (tiempo lineal) es:

 list.RemoveAll(condition); 

Si hay efectos secundarios, usaría algo como:

 var toRemove = new HashSet(); foreach(var item in items) { ... if(condition) toRemove.Add(item); } items.RemoveAll(toRemove.Contains); 

Este sigue siendo el tiempo lineal, suponiendo que el hash es bueno. Pero tiene un mayor uso de memoria debido a la hashset.

Finalmente, si su lista es solo IList lugar de List , sugiero mi respuesta a ¿Cómo puedo hacer este iterador foreach especial? . Esto tendrá un tiempo de ejecución lineal dadas las implementaciones típicas de IList , en comparación con el tiempo de ejecución cuadrático de muchas otras respuestas.

 List TheList = new List(); TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element)); 

No puede usar foreach, pero puede repetir el avance y administrar la variable de índice de bucle cuando elimina un elemento, como se muestra a continuación:

 for (int i = 0; i < elements.Count; i++) { if () { // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation. elements.RemoveAt(i--); } } 

Tenga en cuenta que, en general, todas estas técnicas se basan en el comportamiento de la colección que se itera. La técnica que se muestra aquí funcionará con la lista estándar (T). (Es bastante posible escribir su propia clase de colección e iterador que permite la eliminación de elementos durante un ciclo foreach).

Usar Remove o RemoveAt en una lista mientras se itera sobre esa lista se ha dificultado intencionalmente, porque casi siempre es lo incorrecto . Es posible que pueda hacer que funcione con algún truco inteligente, pero sería extremadamente lento. Cada vez que llamas a Remove debes escanear toda la lista para encontrar el elemento que deseas eliminar. Cada vez que llamas a RemoveAt tiene que mover los elementos siguientes 1 posición hacia la izquierda. Como tal, cualquier solución que use Remove o RemoveAt , requeriría un tiempo cuadrático, O (n²) .

Use RemoveAll si puede. De lo contrario, el siguiente patrón filtrará la lista in situ en tiempo lineal, O (n) .

 // Create a list to be filtered IList elements = new List(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); // Filter the list int kept = 0; for (int i = 0; i < elements.Count; i++) { // Test whether this is an element that we want to keep. if (elements[i] % 3 > 0) { // Add it to the list of kept elements. elements[kept] = elements[i]; kept++; } } // Unfortunately IList has no Resize method. So instead we // remove the last element of the list until: elements.Count == kept. while (kept < elements.Count) elements.RemoveAt(elements.Count-1); 

Me gustaría que el “patrón” fuera algo como esto:

 foreach( thing in thingpile ) { if( /* condition#1 */ ) { foreach.markfordeleting( thing ); } elseif( /* condition#2 */ ) { foreach.markforkeeping( thing ); } } foreachcompleted { // then the programmer's choices would be: // delete everything that was marked for deleting foreach.deletenow(thingpile); // ...or... keep only things that were marked for keeping foreach.keepnow(thingpile); // ...or even... make a new list of the unmarked items others = foreach.unmarked(thingpile); } 

Esto alinearía el código con el proceso que ocurre en el cerebro del progtwigdor.

Al asumir que el predicado es una propiedad booleana de un elemento, si es verdadero, entonces el elemento debe eliminarse:

  int i = 0; while (i < list.Count()) { if (list[i].predicate == true) { list.RemoveAt(i); continue; } i++; } 

Volvería a asignar la lista de una consulta LINQ que filtró los elementos que no deseaba conservar.

 list = list.Where(item => ...).ToList(); 

A menos que la lista sea muy grande, no debería haber problemas significativos de rendimiento al hacerlo.

La mejor manera de eliminar elementos de una lista mientras se itera sobre ella es usar RemoveAll() . Pero la principal preocupación escrita por las personas es que tienen que hacer algunas cosas complejas dentro del ciclo y / o tener casos de comparación complejos.

La solución es seguir usando RemoveAll() pero use esta notación:

 var list = new List(Enumerable.Range(1, 10)); list.RemoveAll(item => { // Do some complex operations here // Or even some operations on the items SomeFunction(item); // In the end return true if the item is to be removed. False otherwise return item > 5; }); 
 foreach(var item in list.ToList()) { if(item.Delete) list.Remove(item); } 

Simplemente crea una lista completamente nueva de la primera. Digo “Fácil” en lugar de “Derecho”, ya que la creación de una lista completamente nueva tiene un rendimiento superior al método anterior (no me he tomado ninguna referencia). Generalmente prefiero este patrón, también puede ser útil para superar Limitaciones de Linq-To-Entities.

 for(i = list.Count()-1;i>=0;i--) { item=list[i]; if (item.Delete) list.Remove(item); } 

De esta forma, se desplaza por la lista hacia atrás con un viejo bucle For. Hacer esto adelante podría ser problemático si el tamaño de la colección cambia, pero al revés siempre debería ser seguro.

Me encontré en una situación similar en la que tuve que eliminar cada n- ésimo elemento en una List dada List .

 for (int i = 0, j = 0, n = 3; i < list.Count; i++) { if ((j + 1) % n == 0) //Check current iteration is at the nth interval { list.RemoveAt(i); j++; //This extra addition is necessary. Without it j will wrap //down to zero, which will throw off our index. } j++; //This will always advance the j counter } 

El costo de eliminar un elemento de la lista es proporcional al número de artículos que siguen al que se eliminará. En el caso en que la primera mitad de los artículos califique para la eliminación, cualquier enfoque que se base en la eliminación de elementos individualmente terminará teniendo que realizar aproximadamente N * N / 4 operaciones de copia de artículos, lo que puede ser muy costoso si la lista es grande .

Un enfoque más rápido es escanear a través de la lista para encontrar el primer elemento que se va a eliminar (si corresponde) y, a partir de ese momento, copiar cada elemento que se debe conservar en el lugar al que pertenece. Una vez hecho esto, si se deben retener los elementos R, los primeros elementos R de la lista serán esos elementos R, y todos los elementos que requieren eliminación se encontrarán al final. Si esos elementos se eliminan en orden inverso, el sistema no terminará teniendo que copiar ninguno de ellos, por lo que si la lista tuviera N elementos de los cuales se retuvieron R artículos, incluidos todos los primeros F, será necesario copie los elementos de RF y reduzca la lista en un elemento NR veces. Todo el tiempo lineal.

Mi enfoque es que primero creo una lista de índices, que debería eliminarse. Luego recorro los índices y elimino los ítems de la lista inicial. Esto se ve así:

 var messageList = ...; // Restrict your list to certain criteria var customMessageList = messageList.FindAll(m => m.UserId == someId); if (customMessageList != null && customMessageList.Count > 0) { // Create list with positions in origin list List positionList = new List(); foreach (var message in customMessageList) { var position = messageList.FindIndex(m => m.MessageId == message.MessageId); if (position != -1) positionList.Add(position); } // To be able to remove the items in the origin list, we do it backwards // so that the order of indices stays the same positionList = positionList.OrderByDescending(p => p).ToList(); foreach (var position in positionList) { messageList.RemoveAt(position); } } 

Copie la lista que está iterando. A continuación, elimine de la copia e integre el original. Retroceder es confuso y no funciona bien cuando se realiza un bucle en paralelo.

 var ids = new List { 1, 2, 3, 4 }; var iterableIds = ids.ToList(); Parallel.ForEach(iterableIds, id => { ids.Remove(id); }); 
 myList.RemoveAt(i--); simples;