web-dev-qa-db-ger.com

Wie entferne ich Elemente aus einer allgemeinen Liste, während ich darüber iteriere?

Ich suche ein besseres Muster um mit einer Liste von Elementen zu arbeiten, die jeweils bearbeitet werden müssen und dann je nach Ergebnis sind von der Liste entfernt.

Sie können .Remove(element) nicht in foreach (var element in X) verwenden (da dies zu Collection was modified; enumeration operation may not execute. Ausnahmen führt) ... Sie können for (int i = 0; i < elements.Count(); i++) und .RemoveAt(i) auch nicht verwenden, weil Ihre aktuelle Position in der Sammlung in Bezug auf i wird gestört.

Gibt es eine elegante Möglichkeit, dies zu tun?

405

Durchlaufen Sie Ihre Liste in umgekehrter Reihenfolge mit einer for-Schleife:

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

Beispiel:

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

Alternativ können Sie die RemoveAll-Methode mit einem Prädikat verwenden, gegen das getestet werden soll:

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

Hier ist ein vereinfachtes Beispiel zur Veranschaulichung:

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));
663
Ahmad Mageed

Eine einfache und unkomplizierte Lösung:

Verwenden Sie eine normale for-Schleife rückwärts für Ihre Sammlung und RemoveAt(i), um Elemente zu entfernen.

83
Jan

Die umgekehrte Iteration sollte als Erstes in den Sinn kommen, wenn Sie Elemente aus einer Sammlung entfernen möchten, während Sie darüber iterieren.

Glücklicherweise gibt es eine elegantere Lösung als das Schreiben einer for-Schleife, die unnötiges Tippen erfordert und fehleranfällig sein kann.

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);
    }
}
64
jedesah
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Wenn Sie ".ToList ()" zu Ihrer Liste hinzufügen (oder die Ergebnisse einer LINQ-Abfrage), können Sie "item" direkt aus "list" entfernen, ohne das gefürchtete " Sammlung wurde geändert; Aufzählungsoperation wird möglicherweise nicht ausgeführt . " Error. Der Compiler erstellt eine Kopie von "list", damit Sie das Array sicher entfernen können.

Obwohl dieses Muster nicht besonders effizient ist, fühlt es sich natürlich an und ist flexibel genug für fast jede Situation. Zum Beispiel, wenn Sie jedes "Element" in einer Datenbank speichern und es nur dann aus der Liste entfernen möchten, wenn das Speichern der Datenbank erfolgreich war.

53
Greg Little

Wenn Sie ToArray () in einer allgemeinen Liste verwenden, können Sie ein Remove (Element) in Ihrer allgemeinen Liste ausführen:

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

Wählen Sie die Elemente aus, die Sie tun möchten, anstatt zu versuchen, die Elemente zu entfernen, die Sie nicht möchten. Dies ist so viel einfacher (und im Allgemeinen auch effizienter) als das Entfernen von Elementen.

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

Ich wollte dies als Antwort auf den Kommentar von Michael Dillon unten posten, aber es ist zu lang und wahrscheinlich nützlich, um es trotzdem in meiner Antwort zu haben:

Persönlich würde ich niemals einzelne Elemente entfernen, wenn Sie sie entfernen müssen. Rufen Sie dann RemoveAll auf, das ein Prädikat annimmt und das interne Array nur einmal neu anordnet, während Remove einen Array.Copy ausführt ] _ Operation für jedes Element, das Sie entfernen. RemoveAll ist wesentlich effizienter.

Und wenn Sie rückwärts über eine Liste iterieren, haben Sie bereits den Index des Elements, das Sie entfernen möchten, so dass es weitaus effizienter wäre, RemoveAt aufzurufen, da Remove zuerst einen Durchlauf von vornimmt Die Liste, um den Index des Elements zu finden, das Sie entfernen möchten, aber den Index kennen Sie bereits.

Alles in allem sehe ich keinen Grund, jemals Remove in einer for-Schleife anzurufen. Und im Idealfall, wenn es überhaupt möglich ist, verwenden Sie den obigen Code, um Elemente nach Bedarf aus der Liste zu streamen, sodass überhaupt keine zweite Datenstruktur erstellt werden muss.

20
JulianR

Mit .ToList () wird eine Kopie Ihrer Liste erstellt, wie in dieser Frage erläutert: ToList () - Erstellt es eine neue Liste?

Mit ToList () können Sie aus Ihrer ursprünglichen Liste entfernen, da Sie tatsächlich über eine Kopie iterieren.

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

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

     }
18
StuartQ

Wenn die Funktion, die festlegt, welche Elemente gelöscht werden sollen, keine Nebenwirkungen hat und das Element nicht mutiert (es ist eine reine Funktion), ist eine einfache und effiziente (lineare Zeit-) Lösung:

list.RemoveAll(condition);

Wenn es Nebenwirkungen gibt, würde ich so etwas verwenden:

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

Dies ist immer noch eine lineare Zeit, vorausgesetzt der Hash ist gut. Aber es hat eine erhöhte Speichernutzung aufgrund des Hashsets.

Schließlich, wenn Ihre Liste nur ein IList<T> anstelle eines List<T> ist, schlage ich meine Antwort vor auf Wie kann ich diesen speziellen foreach-Iterator ausführen? . Dies wird bei typischen Implementierungen von IList<T> eine lineare Laufzeit haben, verglichen mit der quadratischen Laufzeit vieler anderer Antworten.

12
CodesInChaos

Da jedes Entfernen unter einer Bedingung erfolgt, die Sie verwenden können

list.RemoveAll(item => item.Value == someValue);
10
Ahmad
List<T> TheList = new List<T>();

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

Sie können foreach nicht verwenden, aber Sie können vorwärts iterieren und Ihre Schleifenindexvariable verwalten, wenn Sie ein Element entfernen, wie folgt:

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

Beachten Sie, dass im Allgemeinen alle diese Techniken auf dem Verhalten der Sammlung beruhen, die iteriert wird. Die hier gezeigte Technik funktioniert mit der Standardliste (T). (Es ist durchaus möglich, eine eigene Auflistungsklasse und einen Iterator zu schreiben, die erlaubt das Entfernen von Elementen während einer foreach-Schleife.)

7
yoyo

Die Verwendung von Remove oder RemoveAt für eine Liste beim Durchlaufen dieser Liste wurde absichtlich erschwert, da dies fast immer falsches Vorgehen ist. Sie könnten in der Lage sein, es mit einem cleveren Trick zum Laufen zu bringen, aber es wäre extrem langsam. Bei jedem Aufruf von Remove muss die gesamte Liste durchsucht werden, um das zu entfernende Element zu finden. Jedes Mal, wenn Sie RemoveAt aufrufen, muss es nachfolgende Elemente um eine Position nach links verschieben. Daher würde jede Lösung, die Remove oder RemoveAt verwendet, eine quadratische Zeit erfordern, O (n²).

Verwenden Sie RemoveAll, wenn Sie können. Andernfalls filtert das folgende Muster die Liste an Ort und Stelle in linearer Zeit 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);
5
bcmpinc

Die beste Möglichkeit, Elemente aus einer Liste zu entfernen, während Sie sie durchlaufen, ist die Verwendung von RemoveAll(). Die Hauptsorge der Leute ist jedoch, dass sie einige komplexe Dinge innerhalb der Schleife tun und/oder komplexe Vergleichsfälle haben müssen.

Die Lösung ist, weiterhin RemoveAll() zu verwenden, aber diese Notation zu verwenden:

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

Ich wünsche das "Muster" war ungefähr so:

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

Dies würde den Code an dem Prozess ausrichten, der im Gehirn des Programmierers abläuft.

3
warrens

Ich würde die Liste von einer LINQ-Abfrage neu zuweisen, die die Elemente herausfilterte, die Sie nicht behalten wollten.

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

Sofern die Liste nicht sehr umfangreich ist, sollten dabei keine wesentlichen Leistungsprobleme auftreten.

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

{

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

}

Erstellen Sie einfach eine völlig neue Liste aus der ersten. Ich sage "Einfach" und nicht "Richtig", da das Erstellen einer völlig neuen Liste wahrscheinlich eine Leistungsverbesserung gegenüber der vorherigen Methode darstellt (ich habe mich nicht um Benchmarking gekümmert). Ich bevorzuge im Allgemeinen dieses Muster, es kann auch bei der Überwindung hilfreich sein Einschränkungen von Linq-To-Entities.

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

{

item=list[i];

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

}

Auf diese Weise wird die Liste mit einer einfachen alten For-Schleife rückwärts durchlaufen. Dies vorwärts zu tun könnte problematisch sein, wenn sich die Größe der Sammlung ändert, aber rückwärts sollte immer sicher sein.

2
Lijo

Unter der Annahme, dass Prädikat eine boolesche Eigenschaft eines Elements ist, sollte das Element entfernt werden, wenn es wahr ist:

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

In C # besteht eine einfache Möglichkeit darin, diejenigen zu markieren, die Sie löschen möchten, und dann eine neue Liste zu erstellen, über die Sie iterieren können ...

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

oder noch einfacher verwenden linq ....

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

es ist jedoch zu überlegen, ob andere Tasks oder Threads zum gleichen Zeitpunkt Zugriff auf dieselbe Liste haben. Verwenden Sie stattdessen möglicherweise eine ConcurrentList.

0
andrew pate

Ich befand mich in einer ähnlichen Situation, in der ich alle n entfernen mussteth Element in einem gegebenen 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

Mein Ansatz ist, dass ich zuerst eine Liste von Indizes erstelle, die gelöscht werden sollen. Danach durchlaufe ich die Indizes und entferne die Einträge aus der Ausgangsliste. Das sieht so aus:

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
testing

Die Kosten für das Entfernen eines Elements aus der Liste sind proportional zur Anzahl der Elemente, die auf das zu entfernende Element folgen. In dem Fall, in dem die erste Hälfte der Elemente für das Entfernen qualifiziert ist, muss jeder Ansatz, der auf dem Entfernen einzelner Elemente basiert, etwa N · N/4 Elementkopiervorgänge ausführen, was sehr teuer werden kann, wenn die Liste groß ist .

Eine schnellere Methode besteht darin, die Liste zu durchsuchen, um das erste zu entfernende Element (falls vorhanden) zu finden, und von diesem Punkt an jedes Element zu kopieren, das an der Stelle aufbewahrt werden soll, an der es gehört. Wenn danach R-Elemente beibehalten werden sollen, sind die ersten R-Elemente in der Liste diese R-Elemente, und alle zu löschenden Elemente befinden sich am Ende. Wenn diese Elemente in umgekehrter Reihenfolge gelöscht werden, muss das System keines von ihnen kopieren. Wenn die Liste also N Elemente enthält, von denen R Elemente, einschließlich aller ersten F, beibehalten wurden, ist dies erforderlich RF-Elemente kopieren und die Liste um ein NR-Mal verkleinern. Alle lineare Zeit.

0
supercat

Kopieren Sie die Liste, die Sie durchlaufen. Dann von der Kopie entfernen und das Original einbinden. Rückwärts zu gehen ist verwirrend und funktioniert nicht gut, wenn parallel geschleift wird.

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

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