Tengo el caso clásico de intentar eliminar un elemento de una colección mientras lo enumero en un bucle:
List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);
foreach (int i in myIntCollection)
{
if (i == 42)
myIntCollection.Remove(96); // The error is here.
if (i == 25)
myIntCollection.Remove(42); // The error is here.
}
Al comienzo de la iteración después de que ocurre un cambio, InvalidOperationException
se lanza un, porque a los enumeradores no les gusta cuando cambia la colección subyacente.
Necesito hacer cambios en la colección mientras itero. Hay muchos patrones que se pueden usar para evitar esto , pero ninguno parece tener una buena solución:
No elimine dentro de este ciclo, en su lugar mantenga una "Lista de borrado" separada, que procesa después del ciclo principal.
Esta es normalmente una buena solución, pero en mi caso, necesito que el elemento desaparezca instantáneamente como "esperando" hasta que el bucle principal para eliminar realmente el elemento cambie el flujo lógico de mi código.
En lugar de eliminar el elemento, simplemente coloque una bandera en el elemento y márquelo como inactivo. Luego agregue la funcionalidad del patrón 1 para limpiar la lista.
Esto podría funcionar para todas mis necesidades, pero significa que una gran cantidad de código tendrá que cambiar con el fin de comprobar el indicador inactivo cada vez que se accede a un elemento. Esto es demasiada administración para mi gusto.
De alguna manera incorpore las ideas del patrón 2 en una clase de la que se deriva
List<T>
. Esta Superlista manejará la bandera inactiva, la eliminación de objetos después del hecho y tampoco expondrá los elementos marcados como inactivos a los consumidores de enumeración. Básicamente, solo encapsula todas las ideas del patrón 2 (y posteriormente del patrón 1).¿Existe una clase como esta? ¿Alguien tiene un código para esto? ¿O hay un mejor camino?
Me han dicho que acceder en
myIntCollection.ToArray()
lugar demyIntCollection
resolverá el problema y me permitirá eliminar dentro del bucle.Esto me parece un mal patrón de diseño, ¿o tal vez está bien?
Detalles:
La lista contendrá muchos elementos y solo eliminaré algunos de ellos.
Dentro del ciclo, haré todo tipo de procesos, agregar, eliminar, etc., por lo que la solución debe ser bastante genérica.
Es posible que el elemento que necesito eliminar no sea el elemento actual del bucle. Por ejemplo, puedo estar en el elemento 10 de un bucle de 30 elementos y necesito eliminar el elemento 6 o el elemento 26. Caminar hacia atrás a través de la matriz ya no funcionará debido a esto. ; o (
fuente
Respuestas:
La mejor solución suele ser utilizar el
RemoveAll()
método:myList.RemoveAll(x => x.SomeProp == "SomeValue");
O, si necesita eliminar ciertos elementos:
MyListType[] elems = new[] { elem1, elem2 }; myList.RemoveAll(x => elems.Contains(x));
Esto supone que su bucle está destinado únicamente a fines de eliminación, por supuesto. Si haces necesidad de procesamiento adicional, entonces el mejor método es generalmente usar una
for
owhile
bucle, desde entonces no se está usando un enumerador:for (int i = myList.Count - 1; i >= 0; i--) { // Do processing here, then... if (shouldRemoveCondition) { myList.RemoveAt(i); } }
Ir hacia atrás asegura que no se salte ningún elemento.
Respuesta a editar :
Si va a eliminar elementos aparentemente arbitrarios, el método más fácil podría ser simplemente realizar un seguimiento de los elementos que desea eliminar y luego eliminarlos todos a la vez. Algo como esto:
List<int> toRemove = new List<int>(); foreach (var elem in myList) { // Do some stuff // Check for removal if (needToRemoveAnElement) { toRemove.Add(elem); } } // Remove everything here myList.RemoveAll(x => toRemove.Contains(x));
fuente
Si debe enumerar ay
List<T>
eliminar de él, sugiero simplemente usar unwhile
bucle en lugar de unforeach
var index = 0; while (index < myList.Count) { if (someCondition(myList[index])) { myList.RemoveAt(index); } else { index++; } }
fuente
Sé que esta publicación es antigua, pero pensé en compartir lo que funcionó para mí.
Cree una copia de la lista para enumerar, y luego, en el ciclo para cada ciclo, puede procesar los valores copiados y eliminar / agregar / lo que sea con la lista de origen.
private void ProcessAndRemove(IList<Item> list) { foreach (var item in list.ToList()) { if (item.DeterminingFactor > 10) { list.Remove(item); } } }
fuente
Cuando necesite iterar a través de una lista y pueda modificarla durante el ciclo, es mejor que utilice un ciclo for:
for (int i = 0; i < myIntCollection.Count; i++) { if (myIntCollection[i] == 42) { myIntCollection.Remove(i); i--; } }
Por supuesto, debe tener cuidado, por ejemplo, disminuyo
i
cada vez que se elimina un elemento, ya que de lo contrario omitiremos las entradas (una alternativa es retroceder en la lista).Si tiene Linq, debería usarlo
RemoveAll
como ha sugerido dlev.fuente
--i
.A medida que enumera la lista, agregue el que desea MANTENER a una nueva lista. Luego, asigne la nueva lista al
myIntCollection
List<int> myIntCollection=new List<int>(); myIntCollection.Add(42); List<int> newCollection=new List<int>(myIntCollection.Count); foreach(int i in myIntCollection) { if (i want to delete this) /// else newCollection.Add(i); } myIntCollection = newCollection;
fuente
Agreguemos su código:
List<int> myIntCollection=new List<int>(); myIntCollection.Add(42); myIntCollection.Add(12); myIntCollection.Add(96); myIntCollection.Add(25);
Si desea cambiar la lista mientras está en un foreach, debe escribir
.ToList()
foreach(int i in myIntCollection.ToList()) { if (i == 42) myIntCollection.Remove(96); if (i == 25) myIntCollection.Remove(42); }
fuente
Para aquellos a los que pueda ayudar, escribí este método de Extensión para eliminar elementos que coincidan con el predicado y devolver la lista de elementos eliminados.
public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate) { IList<T> removed = new List<T>(); for (int i = source.Count - 1; i >= 0; i--) { T item = source[i]; if (predicate(item)) { removed.Add(item); source.RemoveAt(i); } } return removed; }
fuente
Qué tal si
int[] tmp = new int[myIntCollection.Count ()]; myIntCollection.CopyTo(tmp); foreach(int i in tmp) { myIntCollection.Remove(42); //The error is no longer here. }
fuente
foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }
para cualquier enumerable yList<T>
específicamente admite este método incluso en .NET 2.0.Si está interesado en el alto rendimiento, puede utilizar dos listas. Lo siguiente minimiza la recolección de basura, maximiza la localidad de la memoria y nunca elimina un elemento de una lista, lo cual es muy ineficiente si no es el último elemento.
private void RemoveItems() { _newList.Clear(); foreach (var item in _list) { item.Process(); if (!item.NeedsRemoving()) _newList.Add(item); } var swap = _list; _list = _newList; _newList = swap; }
fuente
Pensé que compartiré mi solución a un problema similar en el que necesitaba eliminar elementos de una lista mientras los procesaba.
Básicamente "foreach" que eliminará el elemento de la lista después de que se haya iterado.
Mi prueba:
var list = new List<TempLoopDto>(); list.Add(new TempLoopDto("Test1")); list.Add(new TempLoopDto("Test2")); list.Add(new TempLoopDto("Test3")); list.Add(new TempLoopDto("Test4")); list.PopForEach((item) => { Console.WriteLine($"Process {item.Name}"); }); Assert.That(list.Count, Is.EqualTo(0));
Resolví esto con un método de extensión "PopForEach" que realizará una acción y luego eliminará el elemento de la lista.
public static class ListExtensions { public static void PopForEach<T>(this List<T> list, Action<T> action) { var index = 0; while (index < list.Count) { action(list[index]); list.RemoveAt(index); } } }
Espero que esto pueda ser útil para cualquiera.
fuente