2016-04-18 12 views
17

Ho bisogno di scorrere su un List<myObject> e rimuovere elementi che rispondono a una determinata condizione.Il modo migliore per scorrere su un elenco e rimuovere elementi da esso?

ho visto questa risposta (https://stackoverflow.com/a/1582317/5077434):

iterare la vostra lista in retromarcia con un ciclo for:

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

Esempio:

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)); 

Ma ho capito che for è meno efficiente di foreach,

così ho pensato di utilizzare il più avanti come segue:

foreach (var item in myList.ToList()) 
{ 
    // if certain condition applies: 
    myList.Remove(item) 
} 

è un metodo migliore rispetto agli altri?

EDIT:

Non voglio usare RemoveAll(...), in quanto v'è una grande quantità di codice all'interno del ciclo, prima la condizione.

+3

"migliore" come in cosa? prestazione? memoria? leggibilità? – Jcl

+3

'for' non è meno efficiente di' foreach'. E invece di un ciclo, puoi usare 'RemoveAll', che ha prestazioni ancora migliori: https://msdn.microsoft.com/en-us/library/wdka673a%28v=vs.110%29.aspx –

+0

@Jcl 1 Migliore rendimento 2. Migliori pratiche di codifica –

risposta

18

Volenti o nolenti bisogna ciclo sulla lista, e il ciclo for è il più un efficiente:

for (int i = safePendingList.Count - 1; i >= 0; --i) 
    if (condition) 
     safePendingList.RemoveAt(i); 

Se si desidera rimuovere in gamma (non nel intera lista), basta modificare per ciclo:

// 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 se è necessario rimuovere gli elementi in avanti looping:

for (int i = 0; i < safePendingList.Count;) // notice ++i abscence 
    if (condition) 
     safePendingList.RemoveAt(i); 
    else 
     i += 1; // ++i should be here 

Al contrario safePendingList.ToList() crea una copia di iniziale safePendingList e questo significa memoria e CPUoverhead:

// safePendingList.ToList() - CPU and Memory overhead (copying) 
    foreach (var item in safePendingList.ToList()) { 
    if (condition) 
     myList.Remove(item); // Overhead: searching 
    } 

Tuttavia, la più ragionevole piano in molti casi si tratta solo di lasciare .Net lavoro per voi:

safePendingList.RemoveAll(item => condition); 
+0

Una cosa da ricordare è che l'uso di 'RemoveAt()' è molto meno efficiente di 'RemoveAll()' perché manterrà mescolato tutti gli elementi in un indice più grande rispetto al rimosso uno. –

+3

'safePendingList.RemoveAll (item => condition);' è il più vicino ad una funzione 'filter'y di ordine superiore – cat

+2

Oltre ad essere breve e leggibile,' RemoveAll' ha il maggior beneficio che richiede tempo lineare, indipendentemente da quanti o quali elementi devono essere rimossi. Le soluzioni basate su 'RemoveAt' possono richiedere un tempo quadratico a causa del bisogno di spostare ripetutamente ampie sezioni della lista. – user2357112

6

Per rimuovere gli elementi con una certa condizione è possibile utilizzare

list.RemoveAll(item => item > 5); 

invece di

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); 
} 
+0

Cosa c'è di sbagliato con' myList.Where (x => x> 5) '? – arrowd

+0

@arrowd - per ottenere lo stesso risultato deve essere 'myList = myList.Where (x => x <= 5) .ToList();' – fubo

+0

Non voglio farlo con LINQ, in quanto la condizione è complessa e è costruito come il ciclo avanza –

4

Il problema è che foreach non può funzionare con la raccolta mentre viene modificato questo porterà a InvalidOperationException.

Uso .ToList() all'interno foreach permette di evitare che ma questo copia l'intera collezione si comporti scorrendo raccolta due volte (prima volta quando esso copia tutti gli elementi e seconda volta per filtrano).

Il ciclo for è un'opzione migliore perché è possibile passare alla raccolta una sola volta. A parte che è possibile filtrare raccolta utilizzando LINQWhere() metodo, qualcosa tra le linee di:

var myFilteredCollection = myList.Where(i => i <= 5).ToList(); 

Se le condizioni sono complesse è possibile spostare questa logica al metodo separato:

private bool IsItemOk(myObject item) 
{ 
    ... your complicated logic. 
    return true; 
} 

e utilizzarlo in la tua domanda LINQ:

var myFilteredCollection = myList.Where(i => IsItemOk(i)).ToList(); 

Un'altra opzione è farlo viceversa. Crea nuova raccolta e aggiungi elementi in base alle condizioni. Che è possibile utilizzare foreach ciclo:

var newList = new List<MyObject>(myList.Count); 

foreach (var item in myList) 
{ 
    // if certain condition does not apply: 
    newList.Add(item); 
} 

P.S.

Ma, come già accennato .RemoveAll() è meglio perché in questo modo non c'è bisogno di 'reinventare la ruota'

4

Nessuno dei due metodi mostra differenze significative. for e RemoveAll sembrano un po 'più veloci (più probabilmente perché si utilizza Remove per la duplicazione dell'elenco e occorre ricrearlo ... utilizzando i tipi di valore, ciò significa anche il raddoppio della memoria necessaria). Ho fatto un po 'di profilazione:

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); 
} 

Il risultato:

A: 00: 00: 04,1179163

B: 00: 00: 07,7417853

C: 00: 00: 04,3525255

Altre corse danno risultati ancora più simili (si noti che questo è per 10 milioni ons su una lista di 10 articoli [facendo così 100 milioni di operazioni] ... una differenza di ~ 3 secondi non è quella che chiamerei "significativa", ma il tuo chilometraggio può variare)

Quindi vai con quello che pensi tu in grado di leggere meglio (io personalmente uso il metodo RemoveAll)

Nota: come far crescere la tua lista (questo a soli 10 elementi), MethodB (la foreach quella che duplica l'elenco) sarà più lenta e le differenze sarà più alto, dal momento che la duplicazione dell'elenco sarà più costosa. Ad esempio, facendo una lista di 1.000 articoli (con la condizione di 500 anziché 5) e 100.000 iterazioni, rende MethodB 34 secondi, mentre entrambi gli altri vengono eseguiti in meno di 2 secondi sulla mia macchina.Quindi sì, sicuramente non utilizzare MethodB :-)

Questo non significa che foreach è meno efficiente di for, significa solo che per l'utilizzo di foreach per rimuovere gli elementi da un elenco, è necessario duplicare detto lista, e questo è costoso.

Questo altro metodo (utilizzando foreach) dovrebbe essere di circa veloce come le for o RemoveAll metodi (si tratta di un piccolo po 'più lento nel mio profilazione -perché si assegna una nuova lista, ho figura-, ma praticamente trascurabile):

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; 
} 

Hai solo bisogno di invertire la condizione.

non sto suggerendo questo metodo, solo dimostrando che foreach non è necessariamente più lento di for

+1

Giusto, se vuoi vedere quale cavallo è più veloce ... E spesso la migliore politica è lasciare.Net fa il suo lavoro - 'RemoveAll', +1 –

+0

@DmitryBychenko Mi aspettavo questo risultato dalla teoria, ma sì, niente come correre i cavalli :-) – Jcl

+0

Non avevo dubbi che tu * avessi saputo *, ma dal momento che c'è" .. Ma ho capito che 'for' è meno efficiente di' foreach' ... "nella domanda, il modo migliore per assicurarsi che il * contrario * sia vero è lasciare correre i cavalli. –

1

Un altro modo potrebbe essere quello di foreach lista, impostare la voce su null e poi fare un LINQ togliere a (item = > null), come già detto, sta a te scegliere il modo che ti aggrada meglio

Problemi correlati