Modo intelligente per rimuovere elementi da un List <T> durante l'enumerazione in C #


87

Ho il classico caso di provare a rimuovere un elemento da una raccolta enumerandolo in un ciclo:

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

All'inizio dell'iterazione dopo che InvalidOperationExceptionè stata apportata una modifica, viene generato un messaggio di errore, perché agli enumeratori non piace quando la raccolta sottostante cambia.

Devo apportare modifiche alla raccolta durante l'iterazione. Esistono molti modelli che possono essere utilizzati per evitare questo problema , ma nessuno di essi sembra avere una buona soluzione:

  1. Non eliminare all'interno di questo ciclo, mantieni invece un "Elenco di eliminazione" separato, che elaborerai dopo il ciclo principale.

    Questa è normalmente una buona soluzione, ma nel mio caso, ho bisogno che l'elemento scompaia immediatamente poiché "in attesa" fino a dopo il ciclo principale per eliminare davvero l'elemento cambia il flusso logico del mio codice.

  2. Invece di eliminare l'elemento, è sufficiente impostare un flag sull'elemento e contrassegnarlo come inattivo. Quindi aggiungi la funzionalità del modello 1 per ripulire l'elenco.

    Questo sarebbe lavoro per tutti i miei bisogni, ma significa che un sacco di codice dovrà cambiare al fine di verificare la bandiera inattiva ogni volta che un oggetto si accede. Questa è decisamente troppa amministrazione per i miei gusti.

  3. In qualche modo incorporare le idee del modello 2 in una classe da cui deriva List<T>. Questo Superlist gestirà il flag inattivo, l'eliminazione di oggetti dopo il fatto e inoltre non esporrà gli elementi contrassegnati come inattivi ai consumatori dell'enumerazione. Fondamentalmente, incapsula solo tutte le idee del modello 2 (e successivamente del modello 1).

    Esiste una classe come questa? Qualcuno ha un codice per questo? O c'è un modo migliore?

  4. Mi è stato detto che l'accesso myIntCollection.ToArray()invece di myIntCollectionrisolverà il problema e mi consentirà di eliminare all'interno del ciclo.

    Questo mi sembra un cattivo design pattern, o forse va bene?

Dettagli:

  • L'elenco conterrà molti elementi e ne rimuoverò solo alcuni.

  • All'interno del ciclo, farò tutti i tipi di processi, aggiungendo, rimuovendo ecc., Quindi la soluzione deve essere abbastanza generica.

  • L'elemento che devo eliminare potrebbe non essere l'elemento corrente nel ciclo. Ad esempio, potrei trovarmi sull'elemento 10 di un ciclo di 30 elementi e dover rimuovere l'elemento 6 o l'elemento 26. Camminare all'indietro attraverso l'array non funzionerà più a causa di ciò. ; o (


Possibili informazioni utili per qualcun altro: L'errore di Evita raccolta è stato modificato (un incapsulamento del modello 1)
George Duckett

Una nota a margine: le liste risparmiano molto tempo (generalmente O (N), dove N è la lunghezza della lista) spostando i valori. Se è davvero necessario un accesso casuale efficiente, è possibile ottenere delle eliminazioni in O (log N), utilizzando un albero binario bilanciato che contiene il numero di nodi nella sottostruttura di cui è la radice. È un BST la cui chiave (l'indice nella sequenza) è implicita.
Palec

Si prega di vedere la risposta: stackoverflow.com/questions/7193294/...
Dabbas

Risposte:


196

La soluzione migliore è solitamente utilizzare il RemoveAll()metodo:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

Oppure, se hai bisogno di rimuovere alcuni elementi:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

Questo presume che il tuo loop sia destinato esclusivamente a scopi di rimozione, ovviamente. Se fai necessità di ulteriori elaborazioni, quindi il metodo migliore è di solito di utilizzare una foro whileloop, da allora non si sta usando un enumeratore:

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

Tornare indietro ti assicura di non saltare nessun elemento.

Risposta alla modifica :

Se hai intenzione di rimuovere elementi apparentemente arbitrari, il metodo più semplice potrebbe essere quello di tenere traccia degli elementi che desideri rimuovere e quindi rimuoverli tutti in una volta. Qualcosa come questo:

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

Per quanto riguarda la tua risposta: devo rimuovere gli elementi immediatamente durante l'elaborazione di quell'elemento, non dopo che l'intero ciclo è stato elaborato. La soluzione che sto usando è NULLARE tutti gli elementi che voglio eliminare immediatamente e cancellarli in seguito. Questa non è una soluzione ideale in quanto devo controllare NULL ovunque, ma funziona.
John Stock

Chiedendosi se 'elem' non è un int, non possiamo usare RemoveAll in questo modo è presente nel codice di risposta modificato.
Utente M

22

Se devi enumerare a List<T>e rimuovere da esso, ti suggerisco di utilizzare semplicemente un whileciclo invece di unforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

Questa dovrebbe essere la risposta accettata secondo me. Ciò ti consente di prendere in considerazione il resto degli elementi nell'elenco, senza ripetere nuovamente un elenco ristretto di elementi da rimuovere.
Slvrfn

13

So che questo post è vecchio, ma ho pensato di condividere ciò che ha funzionato per me.

Crea una copia dell'elenco per l'enumerazione, quindi nel ciclo for each, puoi elaborare i valori copiati e rimuovere / aggiungere / qualunque cosa con l'elenco di origine.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

Ottima idea su ".ToList ()"! Semplice head-slapper, e funziona anche nei casi in cui non stai usando direttamente i meth di stock "Remove ... ()".
galaxis

1
Molto inefficiente però.
nawfal

8

Quando hai bisogno di scorrere un elenco e potresti modificarlo durante il ciclo, allora è meglio usare un ciclo for:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

Ovviamente devi stare attento, ad esempio decremento iogni volta che un elemento viene rimosso altrimenti salteremo le voci (un'alternativa è tornare indietro nell'elenco).

Se hai Linq, dovresti usarlo RemoveAllcome suggerito da dlev.


Funziona solo quando rimuovi l'elemento corrente. Se stai rimuovendo un elemento arbitrario, dovrai controllare se il suo indice era su / prima o dopo l'indice corrente per decidere se farlo --i.
CompuChip

La domanda originale non chiariva che la cancellazione di altri elementi rispetto a quello attuale deve essere supportata, @CompuChip. Questa risposta non è cambiata da quando è stata chiarita.
Palec

@ Palec, ho capito, da qui il mio commento.
CompuChip

5

Mentre enumerate l'elenco, aggiungete quello che volete MANTENERE a un nuovo elenco. Successivamente, assegna il nuovo elenco al filemyIntCollection

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;

3

Aggiungiamo il tuo codice:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

Se desideri modificare l'elenco mentre sei in una foreach, devi digitare .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

1

Per coloro che possono aiutare, ho scritto questo metodo di estensione per rimuovere gli elementi che corrispondono al predicato e restituire l'elenco degli elementi rimossi.

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

0

Che ne dite di

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

Nell'attuale C #, può essere riscritto come foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }per qualsiasi enumerabile e List<T>supporta specificamente questo metodo anche in .NET 2.0.
Palec

0

Se sei interessato a prestazioni elevate, puoi utilizzare due elenchi. Quanto segue minimizza la garbage collection, massimizza la località della memoria e non rimuove mai effettivamente un elemento da un elenco, il che è molto inefficiente se non è l'ultimo 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;
}

0

Ho appena pensato di condividere la mia soluzione a un problema simile in cui avevo bisogno di rimuovere elementi da un elenco durante l'elaborazione.

Quindi fondamentalmente "foreach" rimuoverà l'elemento dall'elenco dopo che è stato iterato.

Il mio test:

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

Ho risolto questo problema con un metodo di estensione "PopForEach" che eseguirà un'azione e quindi rimuoverà l'elemento dall'elenco.

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

Spero che questo possa essere utile a chiunque.

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.