Intelligente Methode zum Entfernen von Elementen aus einer Liste <T> während der Aufzählung in C #

86

Ich habe den klassischen Fall, dass versucht wird, ein Element aus einer Sammlung zu entfernen, während es in einer Schleife aufgelistet wird:

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

Zu Beginn der Iteration nach einer Änderung wird ein InvalidOperationExceptionausgelöst, da Enumeratoren nicht mögen, wenn sich die zugrunde liegende Sammlung ändert.

Ich muss während der Iteration Änderungen an der Sammlung vornehmen. Es gibt viele Muster, die verwendet werden können, um dies zu vermeiden , aber keines von ihnen scheint eine gute Lösung zu haben:

  1. Löschen Sie nicht innerhalb dieser Schleife, sondern führen Sie eine separate „Löschliste“, die Sie nach der Hauptschleife verarbeiten.

    Dies ist normalerweise eine gute Lösung, aber in meinem Fall muss das Element sofort als "Warten" verschwunden sein, bis nach der Hauptschleife, um das Element wirklich zu löschen, der Logikfluss meines Codes geändert wird.

  2. Anstatt das Element zu löschen, setzen Sie einfach ein Flag auf das Element und markieren Sie es als inaktiv. Fügen Sie dann die Funktionalität von Muster 1 hinzu, um die Liste zu bereinigen.

    Dies würde für alle meine Anforderungen funktionieren, bedeutet jedoch, dass sich bei jedem Zugriff auf ein Element viel Code ändern muss, um das inaktive Flag zu überprüfen. Dies ist viel zu viel Verwaltung für meinen Geschmack.

  3. Integrieren Sie die Ideen von Muster 2 irgendwie in eine Klasse, die von abgeleitet ist List<T>. Diese Superliste behandelt das inaktive Flag, das Löschen von Objekten nachträglich und macht auch keine als inaktiv gekennzeichneten Elemente für Aufzählungskonsumenten verfügbar. Grundsätzlich werden nur alle Ideen von Muster 2 (und anschließend von Muster 1) zusammengefasst.

    Gibt es eine solche Klasse? Hat jemand Code dafür? Oder gibt es einen besseren Weg?

  4. Mir wurde gesagt, dass der Zugriff auf myIntCollection.ToArray()anstelle von myIntCollectiondas Problem löst und es mir ermöglicht, innerhalb der Schleife zu löschen.

    Dies scheint mir ein schlechtes Designmuster zu sein, oder ist es in Ordnung?

Einzelheiten:

  • Die Liste wird viele Elemente enthalten und ich werde nur einige davon entfernen.

  • Innerhalb der Schleife werde ich alle Arten von Prozessen ausführen, hinzufügen, entfernen usw., daher muss die Lösung ziemlich allgemein sein.

  • Das Element, das ich löschen muss, ist möglicherweise nicht das aktuelle Element in der Schleife. Zum Beispiel kann ich mich auf Punkt 10 einer 30-Punkte-Schleife befinden und muss Punkt 6 oder Punkt 26 entfernen. Aus diesem Grund funktioniert es nicht mehr, rückwärts durch das Array zu gehen. ;Ö(

John Stock
quelle
Mögliche nützliche Informationen für jemand anderen: Sammlung vermeiden wurde geändert Fehler (eine Kapselung von Muster 1)
George Duckett
Eine Randnotiz: Listen sparen viel Zeit (im Allgemeinen O (N), wobei N die Länge der Liste ist), um die Werte zu verschieben. Wenn ein effizienter Direktzugriff wirklich erforderlich ist, können Löschungen in O (log N) mithilfe eines ausgeglichenen Binärbaums erzielt werden, der die Anzahl der Knoten im Teilbaum enthält, deren Wurzel es ist. Es ist eine BST, deren Schlüssel (der Index in der Sequenz) impliziert ist.
Palec
Bitte sehen Sie die Antwort: stackoverflow.com/questions/7193294/…
Dabbas

Antworten:

195

Die beste Lösung ist normalerweise die Verwendung der folgenden RemoveAll()Methode:

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

Oder wenn Sie bestimmte Elemente entfernen müssen:

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

Dies setzt natürlich voraus, dass Ihre Schleife ausschließlich zum Entfernen bestimmt ist. Wenn Sie tun müssen , um eine zusätzliche Verarbeitung, dann ist die beste Methode , in der Regel eine zu verwenden foroder whileSchleife, da dann sind Sie nicht einen Enumerator mit:

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

Wenn Sie rückwärts gehen, wird sichergestellt, dass Sie keine Elemente überspringen.

Antwort auf Bearbeiten :

Wenn scheinbar willkürliche Elemente entfernt werden sollen, besteht die einfachste Methode darin, nur die Elemente zu verfolgen, die Sie entfernen möchten, und sie anschließend alle auf einmal zu entfernen. Etwas wie das:

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));
dlev
quelle
Zu Ihrer Antwort: Ich muss die Elemente sofort während der Verarbeitung dieses Elements entfernen, nicht nachdem die gesamte Schleife verarbeitet wurde. Die Lösung, die ich verwende, besteht darin, alle Elemente, die ich sofort löschen möchte, auf NULL zu setzen und sie anschließend zu löschen. Dies ist keine ideale Lösung, da ich überall nach NULL suchen muss, aber es funktioniert.
John Stock
Fragen Sie sich, ob 'elem' kein int ist, dann können wir RemoveAll nicht so verwenden, wie es im bearbeiteten Antwortcode vorhanden ist.
Benutzer M
22

Wenn Sie sowohl a aufzählen als auch daraus List<T>entfernen müssen, empfehle ich, einfach eine whileSchleife anstelle von a zu verwendenforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
JaredPar
quelle
Dies sollte meiner Meinung nach die akzeptierte Antwort sein. Auf diese Weise können Sie den Rest der Elemente in Ihrer Liste berücksichtigen, ohne eine Auswahlliste der zu entfernenden Elemente erneut zu wiederholen.
Slvrfn
12

Ich weiß, dass dieser Beitrag alt ist, aber ich dachte, ich würde mitteilen, was für mich funktioniert hat.

Erstellen Sie eine Kopie der Liste zum Aufzählen, und dann können Sie in jeder Schleife die kopierten Werte verarbeiten und mit der Quellliste entfernen / hinzufügen / was auch immer.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}
D-Jones
quelle
Tolle Idee auf der ".ToList ()"! Einfacher Head-Slapper und funktioniert auch in Fällen, in denen Sie die Standardmethode "Remove ... ()" nicht direkt verwenden.
user1172173
Sehr ineffizient.
Nawfal
8

Wenn Sie eine Liste durchlaufen müssen und sie möglicherweise während der Schleife ändern, ist es besser, eine for-Schleife zu verwenden:

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

Natürlich müssen Sie vorsichtig sein, zum Beispiel dekrementiere ich, iwenn ein Element entfernt wird, da wir sonst Einträge überspringen (eine Alternative besteht darin, die Liste rückwärts durchzugehen).

Wenn Sie Linq haben, sollten Sie es einfach so verwenden, RemoveAllwie es dlev vorgeschlagen hat.

Justin
quelle
Funktioniert jedoch nur, wenn Sie das aktuelle Element entfernen. Wenn Sie ein beliebiges Element entfernen, müssen Sie überprüfen, ob sein Index bei / vor oder nach dem aktuellen Index lag, um zu entscheiden, ob dies der Fall ist --i.
CompuChip
Die ursprüngliche Frage machte nicht klar, dass das Löschen anderer als der aktuellen Elemente unterstützt werden muss, @CompuChip. Diese Antwort hat sich seit ihrer Klärung nicht geändert.
Palec
@Palec, ich verstehe, daher mein Kommentar.
CompuChip
5

Fügen Sie beim Auflisten der Liste die Liste hinzu, die Sie behalten möchten, zu einer neuen Liste. Weisen Sie anschließend die neue Liste der zumyIntCollection

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;
James Curran
quelle
3

Fügen wir Ihren Code hinzu:

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

Wenn Sie die Liste ändern möchten, während Sie sich in einem Foreach befinden, müssen Sie Folgendes eingeben .ToList()

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

Wie wäre es mit

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}
Olaf
quelle
In der aktuellen C # kann es wie foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }für jede Aufzählung neu geschrieben werden und List<T>unterstützt diese Methode speziell auch in .NET 2.0.
Palec
0

Wenn Sie an hoher Leistung interessiert sind, können Sie zwei Listen verwenden. Das Folgende minimiert die Speicherbereinigung, maximiert die Speicherlokalität und entfernt niemals ein Element aus einer Liste. Dies ist sehr ineffizient, wenn es nicht das letzte Element ist.

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;
}
Will Calderwood
quelle
0

Für diejenigen, denen es helfen kann, habe ich diese Erweiterungsmethode geschrieben, um Elemente zu entfernen, die mit dem Prädikat übereinstimmen, und die Liste der entfernten Elemente zurückzugeben.

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