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

451

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

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

¿Hay alguna forma elegante de hacer esto?

Aceleración invertida
fuente

Respuestas:

734

Itere su lista a la inversa con un bucle for:

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

Ejemplo:

var list = new List<int>(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:

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

Aquí hay un ejemplo simplificado para demostrar:

var list = new List<int>(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));
Ahmad Mageed
fuente
19
Para aquellos que vienen de Java, la Lista de C # es como ArrayList en que la inserción / eliminación es O (n) y la recuperación a través de un índice es O (1). Esta no es una lista vinculada tradicional. Parece un poco desafortunado que C # use la palabra "Lista" para describir esta estructura de datos, ya que recuerda la clásica lista vinculada.
Jarrod Smith
79
Nada en el nombre 'Lista' dice 'LinkedList'. Las personas que provienen de otros lenguajes que no sean Java pueden confundirse cuando se trata de una lista vinculada.
GolezTrol
55
Terminé aquí a través de una búsqueda de vb.net, así que en caso de que alguien quiera la sintaxis equivalente de vb.net para RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder
1
Esto también funciona en Java con una modificación mínima: en .size()lugar de .County en .remove(i)lugar de .removeAt(i). Clever - gracias!
Otoño Leonard
2
Hice una pequeña prueba de rendimiento y resultó que RemoveAll()lleva tres veces más tiempo que el forciclo hacia atrás . Así que definitivamente me quedo con el ciclo, al menos en las secciones donde importa.
Crusha K. Rool
84

Una solución simple y directa:

Use un bucle for estándar que se ejecute al revés en su colección y RemoveAt(i)para eliminar elementos.

ene
fuente
2
Tenga en cuenta que eliminar elementos de uno en uno no es eficiente si su lista contiene muchos elementos. Tiene el potencial de ser O (n ^ 2). Imagine una lista con dos mil millones de elementos donde simplemente sucede que los primeros mil millones de elementos terminan siendo eliminados. Cada eliminación obliga a copiar todos los elementos posteriores, por lo que terminas copiando mil millones de elementos mil millones de veces cada uno. Esto no se debe a la iteración inversa, se debe a la eliminación de uno en uno. RemoveAll garantiza que cada elemento se copie como máximo una vez para que sea lineal. La eliminación de uno en uno puede ser mil millones de veces más lenta. O (n) versus O (n ^ 2).
Bruce Dawson
71

La iteración inversa debe ser lo primero que se le viene a la mente cuando desea eliminar elementos de una Colección mientras itera sobre ella.

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

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}
jedesah
fuente
77
Esto funcionó perfectamente para mí. Simple y elegante, y requirió cambios mínimos en mi código.
Stephen MacDougall
1
¿Es esto simplemente flippin genio? Y estoy de acuerdo con @StephenMacDougall, no necesito usar esos C ++ 'para bucles y simplemente seguir con LINQ según lo previsto.
Piotr Kula
55
No veo ninguna ventaja sobre foreach simple (int myInt en test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Todavía tiene que asignar una copia para Reverse y presenta ¿Huh? momento: ¿por qué hay reversa?
jahav
11
@jedesah Sí, Reverse<T>()crea un iterador que recorre la lista al revés, pero asigna un búfer adicional con el mismo tamaño que la propia lista para él ( referencesource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>no solo revisa la lista original en orden inverso (sin asignar memoria adicional). Por lo tanto, tanto ToList()y Reverse()tener el mismo consumo de memoria (tanto para crear la copia), pero ToList()no hace nada a los datos. Con Reverse<int>(), me pregunto por qué se invierte la lista, por qué motivo.
jahav
1
@jahav veo tu punto. Es bastante decepcionante que la implementación de Reverse<T>()cree un nuevo búfer, no estoy seguro de entender por qué es necesario. Me parece que dependiendo de la estructura subyacente de la Enumerable, al menos en algunos casos debería ser posible lograr la iteración inversa sin asignar memoria lineal.
jedesah
68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Si agrega ".ToList ()" a su lista (o los resultados de una consulta LINQ), puede eliminar el "elemento" directamente de "lista" sin la temida " Colección modificada; la operación de enumeración puede no ejecutarse ". error. El compilador hace una copia de "lista", para que pueda eliminar de forma segura la matriz.

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

Greg Little
fuente
66
Esta es la mejor solución si la eficiencia no es crucial.
IvanP
2
esto también es más rápido y más legible: list.RemoveAll (i => true);
santiago arizti
1
@ Greg Little, ¿te entendí correctamente? Cuando agregas ToList (), el compilador pasa por la colección copiada pero se elimina del original.
Pyrejkee
Si su lista contiene elementos duplicados y solo desea eliminar el elemento que aparece más adelante en la lista, ¿no eliminará esto el elemento incorrecto?
Tim MB
¿Esto también funciona para una lista de objetos?
Tom el Safadi
23

Seleccione los elementos que no desea en lugar de tratar de eliminar los elementos que no desee. Esto es mucho más fácil (y generalmente más eficiente también) 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 de todos modos es demasiado largo y probablemente útil para tener en mi respuesta:

Personalmente, nunca eliminaría los elementos uno por uno, si necesita eliminarlos, llame a RemoveAllque toma un predicado y solo reorganiza la matriz interna una vez, mientras que Removerealiza una Array.Copyoperación para cada elemento que elimina. RemoveAllEs mucho más eficiente.

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

En general, no veo ninguna razón para llamar Removea un bucle for. E idealmente, si es posible, use el código anterior para transmitir elementos de la lista según sea necesario para que no se tenga que crear una segunda estructura de datos.

JulianR
fuente
1
O esto, o agregue un puntero a los elementos no deseados en una segunda lista, luego, después de que finalice su ciclo, repita la lista de eliminación y úsela para eliminar los elementos.
Michael Dillon
22

El uso de ToArray () en una lista genérica le permite eliminar (elemento) en su lista genérica:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }
Etienne Brouillard
fuente
2
Esto no está mal, pero debo señalar que esto evita la necesidad de crear una segunda lista de "almacenamiento" de los elementos que desea eliminar a expensas de copiar la lista completa en una matriz. Una segunda lista de elementos seleccionados a mano probablemente tendrá menos elementos.
Ahmad Mageed
20

El uso de .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)
        }

     }
StuartQ
fuente
1
Pero desde el punto de vista del rendimiento, está copiando su lista, lo que puede llevar algún tiempo. Manera buena y fácil de hacerlo, pero con un rendimiento no tan bueno
Florian K
12

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<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Todavía es tiempo lineal, suponiendo que el hash sea bueno. Pero tiene un mayor uso de memoria debido al hashset.

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

CodesInChaos
fuente
11

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

list.RemoveAll(item => item.Value == someValue);
Ahmad
fuente
Esa es la mejor solución si el procesamiento no muta el artículo y no tiene efectos secundarios.
CodesInChaos
9
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
Mauricio Ramalho
fuente
8

No puede usar foreach, pero puede iterar hacia adelante y administrar su variable de índice de bucle cuando elimina un elemento, de esta manera:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // 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 repite. La técnica que se muestra aquí funcionará con la Lista estándar (T). (Es muy posible que escribir su propia clase de colección y iterador que hace permitir la extracción elemento durante un bucle foreach.)

yoyó
fuente
6

Usar Removeo RemoveAten una lista mientras se itera sobre esa lista se ha hecho intencionalmente difícil, porque casi siempre es algo incorrecto . Es posible que pueda hacer que funcione con algún truco inteligente, pero sería extremadamente lento. Cada vez que llama Removetiene que escanear toda la lista para encontrar el elemento que desea eliminar. Cada vez que llame RemoveAttiene que mover elementos posteriores 1 posición a la izquierda. Como tal, cualquier solución que use Removeo RemoveAt, requeriría tiempo cuadrático, O (n²) .

Úselo RemoveAllsi 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<int> elements = new List<int>(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);
bcmpinc
fuente
3

Me gustaría que el "patrón" era 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 continúa en el cerebro del programador.

Warrens
fuente
1
Suficientemente fácil. Simplemente cree una matriz de bandera booleana (use un tipo de 3 estados, por ejemplo Nullable<bool>, si desea permitir sin marcar), luego úsela después del foreach para eliminar / conservar elementos.
Dan Bechard
3

Suponiendo que el predicado es una propiedad booleana de un elemento, que 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++;
        }
roeesha
fuente
Le di un voto positivo, ya que a veces podría ser más eficiente moverse por la Lista en orden (no en orden inverso). Tal vez podría detenerse al encontrar el primer elemento que no se eliminará porque la lista está ordenada. (Imagine un 'descanso' donde está el i ++ en este ejemplo.
FrankKrumnow
3

Reasignaría 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 de rendimiento significativos al hacer esto.

Martin Liversage
fuente
3

La mejor manera de eliminar elementos de una lista mientras se repite es utilizarlos 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<int>(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;
});
Hüseyin Yağlı
fuente
2
foreach(var item in list.ToList())

{

if(item.Delete) list.Remove(item);

}

Simplemente cree una lista completamente nueva a partir de la primera. Digo "Fácil" en lugar de "Correcto", ya que crear una lista completamente nueva probablemente tenga un rendimiento superior al método anterior (no me he molestado con ninguna evaluación comparativa). 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 la lista hacia atrás con un bucle For antiguo. Hacer esto hacia adelante podría ser problemático si el tamaño de la colección cambia, pero hacia atrás siempre debe ser seguro.

Lijo
fuente
1

Copie la lista que está iterando. Luego elimine de la copia e interate el original. Retroceder es confuso y no funciona bien cuando se realiza un bucle en paralelo.

var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});
Timothy Gonzalez
fuente
1

Me gustaria esto

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

SALIDA

Fred Jones
Billy TheKid
Gambitier
fuente
0

Me encontré en una situación similar en la que tuve que eliminar cada enésimo elemento de un determinado List<T>.

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
}
Paul Nelson Baker
fuente
0

El costo de eliminar un artículo de la lista es proporcional al número de artículos que siguen al que se eliminará. En el caso de que la primera mitad de los elementos califique para su eliminación, cualquier enfoque que se base en la eliminación individual de elementos terminará teniendo que realizar aproximadamente N * N / 4 operaciones de copia de elementos, 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 eliminará (si lo hay) y, a partir de ese momento, copiar cada elemento que se debe retener en el lugar al que pertenece. Una vez hecho esto, si se deben conservar los elementos R, los primeros elementos R de la lista serán esos elementos R, y todos los elementos que requieren eliminación estará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 tenía N elementos de los cuales R elementos, incluidos todos los primeros F, se conservaron, será necesario copie elementos RF y reduzca la lista por un elemento NR veces. Todo el tiempo lineal.

Super gato
fuente
0

Mi enfoque es que primero creo una lista de índices, que deberían eliminarse. Luego, recorro los índices y elimino los elementos 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<int> positionList = new List<int>();
    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);
    }
}
pruebas
fuente
0

En C #, una manera fácil es marcar las que desea eliminar y luego crear una nueva lista para repetir ...

foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

o incluso más simple usar linq ...

list.RemoveAll(p=>p.Delete);

pero vale la pena considerar si otras tareas o subprocesos tendrán acceso a la misma lista al mismo tiempo que está ocupado eliminando, y tal vez use una lista concurrente.

Andrew Paté
fuente
0

Rastree los elementos que se eliminarán con una propiedad y elimínelos todos después del proceso.

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);
Roberto Mutti
fuente
0

Solo quería agregar mis 2 centavos a esto en caso de que esto ayude a alguien, tuve un problema similar pero necesitaba eliminar varios elementos de una lista de matriz mientras se repetía. la respuesta más votada lo hizo por mí en su mayor parte hasta que me encontré con errores y me di cuenta de que el índice era mayor que el tamaño de la lista de la matriz en algunos casos porque se eliminaban varios elementos pero el índice del bucle no se mantenía seguimiento de eso. Lo arreglé con una simple verificación:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}
Sasha1296
fuente
-4
myList.RemoveAt(i--);

simples;
SO
fuente
1
¿Qué hace simples;aquí?
Denny
66
es un delegado ... rechaza mi respuesta cada vez que se ejecuta
SW
SW ROFL! Me encanta tu comentario.
Erick Brown