Manera inteligente de eliminar elementos de una List <T> mientras se enumeran en C #

87

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, InvalidOperationExceptionse 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:

  1. 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.

  2. 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.

  3. 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?

  4. Me han dicho que acceder en myIntCollection.ToArray()lugar de myIntCollectionresolverá 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 (

John Stock
fuente
Posible información útil para otra persona: Evitar colección se ha modificado el error (una encapsulación del patrón 1)
George Duckett
Una nota al margen: las listas ahorran mucho tiempo (generalmente O (N), donde N es la longitud de la lista) moviendo los valores. Si realmente se necesita un acceso aleatorio eficiente, es posible lograr eliminaciones en O (log N), usando un árbol binario balanceado que contenga el número de nodos en el subárbol cuya raíz es. Es un BST cuya clave (el índice en la secuencia) está implícita.
Palec
Consulte la respuesta: stackoverflow.com/questions/7193294/…
Dabbas

Respuestas:

196

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 foro whilebucle, 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));
dlev
fuente
Con respecto a su respuesta: necesito eliminar los elementos instantáneamente durante el procesamiento de ese elemento, no después de que se haya procesado todo el ciclo. La solución que estoy usando es NULAR cualquier elemento que desee eliminar instantáneamente y eliminarlos después. Esta no es una solución ideal ya que tengo que verificar NULL en todo el lugar, pero FUNCIONA.
John Stock
Preguntándonos si 'elem' no es un int, entonces no podemos usar RemoveAll de esa manera está presente en el código de respuesta editado.
Usuario M
22

Si debe enumerar ay List<T>eliminar de él, sugiero simplemente usar un whilebucle en lugar de unforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
JaredPar
fuente
Esta debería ser la respuesta aceptada en mi opinión. Esto le permite considerar el resto de los elementos de su lista, sin tener que repetir una lista corta de elementos que se eliminarán.
Slvrfn
13

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);
        }
    }
}
D-Jones
fuente
¡Gran idea en ".ToList ()"! Sencillo golpe de cabeza, y también funciona en los casos en los que no está utilizando directamente las metanfetaminas "Eliminar ... ()".
galaxis
1
Aunque muy ineficiente.
nawfal
8

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 icada 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 RemoveAllcomo ha sugerido dlev.

Justin
fuente
Sin embargo, solo funciona cuando está eliminando el elemento actual. Si está eliminando un elemento arbitrario, deberá verificar si su índice estaba en / antes o después del índice actual para decidir si hacerlo --i.
CompuChip
La pregunta original no dejaba claro que la eliminación de otros elementos además del actual debe ser compatible, @CompuChip. Esta respuesta no ha cambiado desde que se aclaró.
Palec
@Palec, lo entiendo, de ahí mi comentario.
CompuChip
5

A medida que enumera la lista, agregue el que desea MANTENER a una nueva lista. Luego, asigne la nueva lista almyIntCollection

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;
James Curran
fuente
3

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);
}
Cristian Voiculescu
fuente
1

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;
    }
kvb
fuente
0

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.
}
Olaf
fuente
En C # actual, se puede reescribir como foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }para cualquier enumerable y List<T>específicamente admite este método incluso en .NET 2.0.
Palec
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;
}
Will Calderwood
fuente
0

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.

Markus Knappen Johansson
fuente