Come rimuovere elementi da un elenco generico mentre si scorre su di esso?


451

Sto cercando un modello migliore per lavorare con un elenco di elementi che ciascuno ha bisogno di essere elaborato e quindi a seconda del risultato vengono rimossi dall'elenco.

Non è possibile utilizzare .Remove(element)all'interno di un foreach (var element in X)(perché si traduce in Collection was modified; enumeration operation may not execute.un'eccezione) ... inoltre non è possibile utilizzare for (int i = 0; i < elements.Count(); i++)e .RemoveAt(i)perché interrompe la posizione corrente nella raccolta rispetto a i.

Esiste un modo elegante per farlo?

Risposte:


734

Iterate la vostra lista al contrario 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));

In alternativa, puoi usare il metodo RemoveAll con un predicato per testare contro:

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

Ecco un esempio semplificato per dimostrare:

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

19
Per quelli che provengono da Java, l'elenco di C # è come ArrayList in quanto l'inserimento / rimozione è O (n) e il recupero tramite un indice è O (1). Questo non è un tradizionale elenco di link. Sembra un po 'sfortunato che C # utilizzi la parola "Elenco" per descrivere questa struttura di dati poiché ricorda il classico elenco collegato.
Jarrod Smith,

79
Nulla nel nome 'Elenco' dice 'LinkedList'. Le persone provenienti da lingue diverse da Java potrebbero essere confuse quando si tratterebbe di un elenco collegato.
GolezTrol,

5
Sono finito qui attraverso una ricerca su vb.net, quindi nel caso qualcuno volesse la sintassi equivalente di vb.net per RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder

1
Questo funziona anche in Java con modifiche minime: .size()invece di .Counte .remove(i)invece di .removeAt(i). Clever - grazie!
Autunno Leonard,

2
Ho fatto un piccolo test per le prestazioni e si è scoperto che RemoveAll()richiede tre volte più tempo del forloop indietro . Quindi sono sicuramente fedele al ciclo, almeno nelle sezioni in cui è importante.
Crusha K. Rool,

84

Una soluzione semplice e diretta:

Utilizzare un ciclo standard for running all'indietro sulla raccolta e RemoveAt(i)per rimuovere elementi.


2
Tenere presente che la rimozione di elementi uno alla volta non è efficace se l'elenco contiene molti elementi. Ha il potenziale per essere O (n ^ 2). Immagina un elenco con due miliardi di articoli in cui accade che il primo miliardo di elementi finisca per essere eliminato. Ogni rimozione impone la copia di tutti gli elementi successivi, quindi si finisce per copiare un miliardo di elementi un miliardo di volte ciascuno. Questo non è a causa dell'iterazione inversa, è a causa della rimozione una alla volta. RemoveAll assicura che ogni elemento venga copiato al massimo una volta in modo che sia lineare. La rimozione alla volta può essere un miliardo di volte più lenta. O (n) contro O (n ^ 2).
Bruce Dawson,

71

L'iterazione inversa dovrebbe essere la prima cosa che viene in mente quando si desidera rimuovere elementi da una raccolta mentre si scorre su di essa.

Fortunatamente, esiste una soluzione più elegante rispetto alla scrittura di un ciclo for che comporta la digitazione inutile e può essere soggetto a errori.

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

7
Ha funzionato perfettamente per me. Semplice ed elegante e ha richiesto modifiche minime al mio codice.
Stephen MacDougall,

1
Questo è solo un genio lanciante? E sono d'accordo con @StephenMacDougall, non ho bisogno di usare quei C ++ 'y per i loop e andare avanti con LINQ come previsto.
Piotr Kula,

5
Non vedo alcun vantaggio rispetto a foreach semplice (int myInt in test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Devi ancora allocare una copia per Reverse e introduce Huh? momento - perché c'è Reverse.
Jahav,

11
@jedesah Sì, Reverse<T>()crea un iteratore che scorre l'elenco indietro, ma alloca buffer aggiuntivo con le stesse dimensioni dell'elenco stesso ( riferimentiource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>non passa semplicemente attraverso l'elenco originale in ordine inverso (senza allocare memoria aggiuntiva). Pertanto entrambi ToList()e Reverse()hanno lo stesso consumo di memoria (entrambi creano copia), ma ToList()non fanno nulla per i dati. Con Reverse<int>(), mi chiedo perché la lista sia invertita, per quale motivo.
Jahav,

1
@jahav Capisco il tuo punto. Questo è abbastanza deludente che l'implementazione di Reverse<T>()crea un nuovo buffer, non sono del tutto sicuro di capire perché sia ​​necessario. Mi sembra che, a seconda della struttura sottostante Enumerable, dovrebbe almeno essere possibile in alcuni casi ottenere iterazione inversa senza allocare memoria lineare.
jedesah,

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

Se aggiungi ".ToList ()" al tuo elenco (o ai risultati di una query LINQ), puoi rimuovere "item" direttamente da "list" senza la temuta " Raccolta è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita ." errore. Il compilatore crea una copia di "list", in modo da poter eseguire in sicurezza la rimozione sull'array.

Sebbene questo modello non sia super efficiente, ha una sensazione naturale ed è abbastanza flessibile per quasi ogni situazione . Ad esempio quando si desidera salvare ciascun "elemento" in un DB e rimuoverlo dall'elenco solo quando il salvataggio del DB ha esito positivo.


6
Questa è la soluzione migliore se l'efficienza non è cruciale.
IvanP,

2
questo è più veloce e anche più leggibile: list.RemoveAll (i => true);
santiago arizti,

1
@Greg Little, ti ho capito bene - quando aggiungi ToList () il compilatore passa attraverso la raccolta copiata ma rimuove dall'originale?
Pyrejkee,

Se il tuo elenco contiene elementi duplicati e desideri rimuovere l'elemento che si verifica successivamente nell'elenco, ciò non rimuoverà l'elemento sbagliato?
Tim MB

Funziona anche con un elenco di oggetti?
Tom el Safadi,

23

Selezionare gli elementi che non desiderate piuttosto che cercare di rimuovere gli elementi che non si desidera. Questo è molto più semplice (e generalmente anche più efficiente) rispetto alla rimozione di elementi.

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

Volevo pubblicare questo come commento in risposta al commento lasciato da Michael Dillon di seguito, ma è troppo lungo e probabilmente utile per avere comunque nella mia risposta:

Personalmente, non rimuoverei mai gli elementi uno a uno, se hai bisogno di rimuoverli, quindi chiama RemoveAllche accetta un predicato e riorganizza l'array interno solo una volta, mentre Removeesegue Array.Copyun'operazione per ogni elemento rimosso. RemoveAllè molto più efficiente.

E quando stai iterando all'indietro su un elenco, hai già l'indice dell'elemento che vuoi rimuovere, quindi sarebbe molto più efficiente chiamare RemoveAt, perché Removeprima fa un attraversamento dell'elenco per trovare l'indice dell'elemento che stai cercando di rimuovere, ma conosci già quell'indice.

Quindi, tutto sommato, non vedo alcun motivo per mai chiamare Removein un for-loop. E idealmente, se possibile, utilizzare il codice sopra per eseguire lo streaming degli elementi dall'elenco in base alle esigenze, quindi non è necessario creare alcuna seconda struttura di dati.


1
O questo, o aggiungere un puntatore agli elementi indesiderati in un secondo elenco, quindi al termine del ciclo, ripetere l'elenco di rimozione e utilizzarlo per rimuovere gli elementi.
Michael Dillon,

22

L'uso di ToArray () in un elenco generico consente di eseguire una rimozione (elemento) nell'elenco generico:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }

2
Questo non è sbagliato, ma devo sottolineare che questo elude la necessità di creare un secondo elenco di "archiviazione" degli elementi che si desidera rimuovere a spese della copia dell'intero elenco in un array. Un secondo elenco di elementi scelti a mano avrà probabilmente meno elementi.
Ahmad Mageed,

20

L'uso di .ToList () farà una copia del tuo elenco, come spiegato in questa domanda: ToList () - Crea un nuovo elenco?

Utilizzando ToList (), è possibile rimuovere dall'elenco originale, poiché in realtà si sta ripetendo una copia.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }

1
Ma dal punto di vista delle prestazioni stai copiando il tuo elenco, che potrebbe richiedere del tempo. Modo buono e semplice per farlo, ma con una prestazione non così buona
Florian K

12

Se la funzione che determina quali elementi eliminare non ha effetti collaterali e non muta l'elemento (è una funzione pura), una soluzione semplice ed efficiente (tempo lineare) è:

list.RemoveAll(condition);

Se ci sono effetti collaterali, userei qualcosa del tipo:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Questo è ancora tempo lineare, supponendo che l'hash sia buono. Ma ha un uso maggiore della memoria a causa dell'hashset.

Infine, se la tua lista è solo una IList<T>anziché una, List<T>suggerisco la mia risposta a Come posso fare questo speciale iteratore foreach? . Questo avrà un runtime lineare date le implementazioni tipiche di IList<T>, rispetto al runtime quadratico di molte altre risposte.


11

Poiché qualsiasi rimozione è presa a condizione che tu possa usare

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

Questa è la soluzione migliore se l'elaborazione non muta l'articolo e non ha effetti collaterali.
CodesInCos

9
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));

8

Non puoi usare foreach, ma puoi iterare in avanti e gestire la variabile dell'indice del loop quando rimuovi un elemento, in questo modo:

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

Si noti che in generale tutte queste tecniche si basano sul comportamento della raccolta che viene ripetuta. La tecnica mostrata qui funzionerà con l'elenco standard (T). (E 'possibile scrivere la propria classe la raccolta e l'iteratore che fa permettere la rimozione oggetto nel corso di un ciclo foreach.)


6

L'uso Removeo RemoveAtsu un elenco durante l'iterazione di tale elenco è stato intenzionalmente reso difficile, poiché è quasi sempre la cosa sbagliata da fare . Potresti riuscire a farlo funzionare con qualche trucco intelligente, ma sarebbe estremamente lento. Ogni volta che chiami Remove, deve scansionare l'intero elenco per trovare l'elemento che desideri rimuovere. Ogni volta che chiami RemoveAt, deve spostare gli elementi successivi di 1 posizione a sinistra. Pertanto, qualsiasi soluzione che utilizza Removeo RemoveAtrichiederebbe un tempo quadratico, O (n²) .

Usa RemoveAllse puoi. Altrimenti, il modello seguente filtra l'elenco sul posto in tempo lineare, 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);

3

Vorrei che il "modello" fosse qualcosa del genere:

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

Ciò allinea il codice con il processo che si svolge nel cervello del programmatore.


1
Abbastanza facile. Basta creare un array di flag booleani (utilizzare un tipo a 3 stati, ad esempio Nullable<bool>, se si desidera consentire non contrassegnati), quindi utilizzarlo dopo la foreach per rimuovere / conservare gli articoli.
Dan Bechard,

3

Supponendo che il predicato sia una proprietà booleana di un elemento, che se è vero, allora l'elemento dovrebbe essere rimosso:

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }

Ho dato a questo un voto in quanto a volte potrebbe essere più efficiente spostarsi nella Lista in ordine (non in ordine inverso). Forse potresti fermarti quando trovi il primo elemento da non rimuovere perché l'elenco è ordinato. (Immagina una "pausa" in cui l'i ++ è in questo esempio.
FrankKrumnow,

3

Riassegnerei l'elenco da una query LINQ che filtrava gli elementi che non volevi conservare.

list = list.Where(item => ...).ToList();

A meno che l'elenco non sia molto ampio, non dovrebbero esserci problemi di prestazioni significativi nel fare ciò.


3

Il modo migliore per rimuovere elementi da un elenco mentre si scorre su di esso è utilizzare RemoveAll(). Ma la preoccupazione principale scritta dalle persone è che devono fare alcune cose complesse all'interno del ciclo e / o avere casi di confronto complessi.

La soluzione è continuare a utilizzare RemoveAll()ma utilizzare questa notazione:

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

2
foreach(var item in list.ToList())

{

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

}

Basta creare un elenco completamente nuovo dal primo. Dico "Facile" piuttosto che "Giusto" in quanto la creazione di un elenco completamente nuovo probabilmente comporta un premio in termini di prestazioni rispetto al metodo precedente (non mi sono preoccupato di alcun benchmarking). In genere preferisco questo modello, può anche essere utile per superare Limitazioni di Linq-to-Entities.

for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

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

}

In questo modo scorre l'elenco indietro con un semplice ciclo For. Fare questo in avanti potrebbe essere problematico se le dimensioni della collezione cambiano, ma all'indietro dovrebbe sempre essere sicuro.


1

Copia l'elenco che stai ripetendo. Quindi rimuovere dalla copia e interagire con l'originale. Andare all'indietro è confuso e non funziona bene quando si esegue il ciclo in parallelo.

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

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});

1

Vorrei fare questo

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

PRODUZIONE

Fred Jones
Billy TheKid

0

Mi sono trovato in una situazione simile in cui ho dovuto togliere ogni n esimo elemento in un dato 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
}

0

Il costo della rimozione di un elemento dall'elenco è proporzionale al numero di elementi successivi a quello da rimuovere. Nel caso in cui la prima metà degli articoli si qualifichi per la rimozione, qualsiasi approccio basato sulla rimozione individuale degli articoli finirà per dover eseguire circa N * N / 4 operazioni di copia degli oggetti, che possono diventare molto costose se l'elenco è grande .

Un approccio più rapido consiste nell'esplorare l'elenco per trovare il primo elemento da rimuovere (se presente), quindi da quel punto in avanti copiare ogni elemento che deve essere conservato nel punto in cui appartiene. Una volta fatto ciò, se gli articoli R devono essere conservati, i primi elementi R nell'elenco saranno quegli elementi R e tutti gli elementi che richiedono l'eliminazione saranno alla fine. Se tali elementi vengono eliminati in ordine inverso, il sistema non dovrà necessariamente copiarne uno, quindi se l'elenco avesse N elementi di cui R elementi, incluso tutto il primo F, sarebbero stati mantenuti, sarà necessario copiare gli articoli RF e ridurre l'elenco di un elemento NR volte. Tutto il tempo lineare.


0

Il mio approccio è quello di creare prima un elenco di indici, che dovrebbero essere eliminati. Successivamente giro gli indici e rimuovo gli elementi dall'elenco iniziale. Questo assomiglia a questo:

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

0

In C # un modo semplice è quello di contrassegnare quelli che si desidera eliminare, quindi creare un nuovo elenco per scorrere su ...

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

o ancora più semplice usare linq ....

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

ma vale la pena considerare se altre attività o thread avranno accesso allo stesso elenco nello stesso momento in cui si è impegnati a rimuovere e si potrebbe utilizzare invece un ConcurrentList.


0

Traccia gli elementi da rimuovere con una proprietà e rimuovili tutti dopo il processo.

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

0

Volevo solo aggiungere i miei 2 centesimi a questo nel caso questo aiutasse qualcuno, ho avuto un problema simile ma avevo bisogno di rimuovere più elementi da un elenco di array mentre veniva ripetuto. la risposta con il voto più alto l'ha fatto per la maggior parte fino a quando ho riscontrato errori e ho realizzato che l'indice era maggiore della dimensione dell'elenco di array in alcuni casi perché venivano rimossi più elementi ma l'indice del ciclo non manteneva traccia di quello. Ho risolto questo problema con un semplice controllo:

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
}

-4
myList.RemoveAt(i--);

simples;

1
Che cosa simples;fa qui?
Denny,

6
è un delegato ... declassa la mia risposta ogni volta che corre
SW

SW ROFL! Devi amare il tuo commento.
Erick Brown,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.