c# - ¿La mejor manera de iterar sobre una lista y eliminar elementos de ella?
iteration (5)
Necesito iterar sobre una List<myObject>
y eliminar los elementos que responden a una determinada condición.
Vi esta respuesta ( https://stackoverflow.com/a/1582317/5077434 ):
Iterar su lista al revés 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));
Pero entendí que for
es menos eficiente que for
cada
Así que pensé en usar el siguiente como sigue:
foreach (var item in myList.ToList())
{
// if certain condition applies:
myList.Remove(item)
}
¿Es un método mejor que el otro?
EDITAR:
No quiero usar RemoveAll(...)
, ya que hay una gran cantidad de código dentro del bucle, antes de la condición.
El problema es que foreach
no puede trabajar con la colección mientras se está modificando, esto llevará a InvalidOperationException
.
El uso de .ToList()
dentro de foreach
nos permite evitarlo, pero esto copia la colección completa, lo que se traduce en un bucle a través de la colección dos veces (la primera vez cuando copia todos los elementos y la segunda vez para filtrarla).
El bucle for
es la mejor opción porque podemos pasar por la recopilación una vez. Aparte de eso, puede filtrar la colección usando el método LINQ
Where()
, algo entre las líneas de:
var myFilteredCollection = myList.Where(i => i <= 5).ToList();
Si sus condiciones son complejas, puede mover esta lógica a un método separado:
private bool IsItemOk(myObject item)
{
... your complicated logic.
return true;
}
Y utilízalo en tu consulta LINQ:
var myFilteredCollection = myList.Where(i => IsItemOk(i)).ToList();
Otra opción es hacerlo viceversa. Crear nueva colección y agregar elementos basados en la condición. De lo que puede usar foreach
loop:
var newList = new List<MyObject>(myList.Count);
foreach (var item in myList)
{
// if certain condition does not apply:
newList.Add(item);
}
PD
Pero como ya se mencionó .RemoveAll()
es mejor porque de esta manera no tienes que ''reinventar la rueda''
Ninguno de los métodos muestra diferencias significativas. for
y RemoveAll
parece un poco más rápido (lo más probable es que al usar Remove
está duplicando su lista y necesite recrearla ... usando tipos de valor, esto también significa duplicar la memoria necesaria). Hice un poco de perfil
public static void MethodA()
{
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);
}
}
public static void MethodB()
{
var list = new List<int>(Enumerable.Range(1, 10));
foreach (var item in list.ToList())
{
if (item > 5)
list.Remove(item);
}
}
public static void MethodC()
{
var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(x => x > 5);
}
public static void Measure(string meas, Action act)
{
GC.Collect();
GC.WaitForPendingFinalizers();
var st = new Stopwatch();
st.Start();
for (var i = 0; i < 10000000; i++)
act();
st.Stop();
Console.WriteLine($"{meas}: {st.Elapsed}");
}
static void Main(string[] args)
{
Measure("A", MethodA);
Measure("B", MethodB);
Measure("C", MethodC);
}
El resultado:
A: 00: 00: 04.1179163
B: 00: 00: 07.7417853
C: 00: 00: 04.3525255
Otras ejecuciones dan resultados aún más similares (tenga en cuenta que esto es para 10 millones de iteraciones en una lista de 10 elementos [haciendo así 100 millones de operaciones] ... una diferencia de ~ 3 segundos no es lo que yo llamaría "significativo", pero su el kilometraje puede variar)
Vaya con lo que cree que puede leer mejor (yo personalmente usaría el método RemoveAll
)
Nota : a medida que aumente su lista (esto es solo 10 elementos), el MethodB
(el único que duplica la lista) será más lento y las diferencias serán mayores, ya que la duplicación de la lista será más costosa. Por ejemplo, al hacer una lista de 1,000 artículos (con la condición en 500 en lugar de 5) y 100,000 iteraciones, el MethodB
34 segundos, mientras que los otros dos funcionan en menos de 2 segundos en mi máquina. Así que sí, definitivamente no uses MethodB
:-)
Esto no significa que foreach
sea menos eficiente que for
, solo significa que para usar foreach
para eliminar elementos de una lista, debe duplicar dicha lista, y eso es costoso.
Este otro método (usando foreach
) debería ser tan rápido como los métodos for
o RemoveAll
(es un poco más lento en mi perfil, porque asigna una nueva lista, me imagino), pero bastante insignificante):
public static void MethodD()
{
var originalList = new List<int>(Enumerable.Range(1, 1000));
var list = new List<int>(originalList.Count);
foreach (var item in originalList)
{
if (item <= 500)
list.Add(item);
}
list.Capacity = list.Count;
}
Solo necesitas revertir la condición.
No estoy sugiriendo este método, solo estoy probando que foreach
no es necesariamente más lento que for
Otra forma podría ser obtener la lista, establecer el elemento en nulo y luego hacer una eliminación de linq a (elemento => nulo), como ya se dijo, depende de usted elegir la forma que más le convenga.
Willy-nilly, tiene que recorrer la lista, y el bucle for
es el más eficiente:
for (int i = safePendingList.Count - 1; i >= 0; --i)
if (condition)
safePendingList.RemoveAt(i);
Si desea eliminar dentro del rango (no en la lista completa ), simplemente modifique para bucle:
// No Enumarable.Range(1, 10) - put them into "for"
for (int i = Math.Min(11, safePendingList.Count - 1); i >= 1; --i)
if (condition)
safePendingList.RemoveAt(i);
O si tiene que eliminar elementos en bucle hacia adelante :
for (int i = 0; i < safePendingList.Count;) // notice ++i abscence
if (condition)
safePendingList.RemoveAt(i);
else
i += 1; // ++i should be here
Por el contrario, safePendingList.ToList()
crea una copia de safePendingList
inicial y esto significa memoria y sobrecarga de CPU :
// safePendingList.ToList() - CPU and Memory overhead (copying)
foreach (var item in safePendingList.ToList()) {
if (condition)
myList.Remove(item); // Overhead: searching
}
Sin embargo, el plan más razonable en muchos casos es dejar que .Net trabaje para usted:
safePendingList.RemoveAll(item => condition);
para eliminar elementos con una determinada condición que puede utilizar
list.RemoveAll(item => item > 5);
en lugar de
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);
}