Der beste Weg, um Elemente aus einer Sammlung zu entfernen


74

Was ist der beste Weg, um Elemente aus einer Sammlung in C # zu entfernen, sobald das Element bekannt ist, aber nicht der Index. Dies ist eine Möglichkeit, aber es scheint bestenfalls unelegant.

//Remove the existing role assignment for the user.
int cnt = 0;
int assToDelete = 0;
foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name == shortName)
    {
        assToDelete = cnt;
    }
    cnt++;
}
workspace.RoleAssignments.Remove(assToDelete);

Was ich wirklich gerne tun würde, ist das zu entfernende Element nach Eigenschaft (in diesem Fall Name) zu finden, ohne die gesamte Sammlung zu durchlaufen und 2 zusätzliche Variablen zu verwenden.


78
Ich liebe die Variablennamen. Ich würde es hassen, das @ss zu sein, das gelöscht wird.
Tvanfosson

Fügen Sie bei erfolgreicher Suche eine break-Anweisung hinzu, wenn Sie dies auf diese Weise planen. Die Verwendung eines Wörterbuchs ist jedoch wahrscheinlich ohnehin besser, wenn Sie immer nach dem Mitgliedsnamen suchen.
Tvanfosson

Ich denke, Sie haben RemoveAt () in Ihrem Code-Snippet gemeint, da Sie den Index übergeben. Sobald das Element bekannt ist, können Sie Remove () direkt aufrufen.
Hurst

Diese Frage sollte geklärt werden. Mit welchem ​​.Net-Framework befassen sich die Antworten? Sprechen wir über List <T> oder eine andere Struktur, die IList <T> implementiert? - Dies sollte wahrscheinlich in "Was ist der beste Weg, um Elemente aus einer List <T> in .net 3.0 zu entfernen?" Umbenannt werden.
Sam Saffron

Antworten:


28

Wenn Sie über eine ihrer Eigenschaften auf Mitglieder der Sammlung zugreifen möchten, können Sie ein Dictionary<T>oder KeyedCollection<T>stattdessen verwenden. Auf diese Weise müssen Sie nicht nach dem gesuchten Artikel suchen.

Andernfalls könnten Sie zumindest Folgendes tun:

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name == shortName)
    {
        workspace.RoleAssignments.Remove(spAssignment);
        break;
    }
}

13
Dies würde eine Ausnahme verursachen, da Sie die Sammlung während der Verwendung
ändern

14
Nein, tut es nicht. Dies liegt daran, dass nach dem Entfernen des Elements eine Pause eingelegt wird.
JFS

25
Jusst für den Fall, dass man dies liest und auf eine Situation anwendet, in der man mehrere Elemente entfernt, die mehreren Indizes in einem Array speichert und eine separate for-Schleife verwendet, die das Löscharray rückwärts durchläuft, um die Elemente zu löschen.
Robert C. Barth

Oder filtern Sie einfach mit workspace.RoleAssignments = workspace.RoleAssignments.Where (x => x.Member.Name! = ShortName) .ToList ();. Aber ich bevorzuge Liam's Antwort, wenn es möglich ist.
David

138

Wenn RoleAssignments a ist List<T>, können Sie den folgenden Code verwenden.

workSpace.RoleAssignments.RemoveAll(x =>x.Member.Name == shortName);

Ich denke, es ist nur RemoveAll, das den Funktionsparameter hat. Zumindest kann ich es nicht mit Entfernen erstellen.
MichaelGG

5
Ja, es ist RemoveAll. Ich habe tatsächlich die Zeit damit verbracht, dies zu überprüfen, überprüft, ob es sich um RemoveAll handelt, und immer noch in Remove eingefügt. Schade, dass Stackoverflow keinen eingebauten Compiler hat :)
JaredPar

Das erzeugt eine neue Liste, wenn ich mich nicht irre.
Richard Nienaber

4
AFAIK das ist eine In-Place-Entfernung.
Jon Limjap

23

@smaclell fragte in einem Kommentar zu @ sambo99, warum die umgekehrte Iteration effizienter sei.

Manchmal ist es effizienter. Stellen Sie sich vor, Sie haben eine Personenliste und möchten alle Kunden mit einer Bonität <1000 entfernen oder filtern.

Wir haben die folgenden Daten

"Bob" 999
"Mary" 999
"Ted" 1000

Wenn wir vorwärts iterieren würden, würden wir bald in Schwierigkeiten geraten

for( int idx = 0; idx < list.Count ; idx++ )
{
    if( list[idx].Rating < 1000 )
    {
        list.RemoveAt(idx); // whoops!
    }
}

Bei idx = 0 entfernen wir Bob, wodurch alle verbleibenden Elemente nach links verschoben werden . Das nächste Mal durch die Schleife ist idx = 1, aber Liste [1] ist jetzt Tedstatt Mary. Wir überspringen Maryversehentlich. Wir könnten eine while-Schleife verwenden und weitere Variablen einführen.

Oder wir kehren einfach die Iteration um:

for (int idx = list.Count-1; idx >= 0; idx--)
{
    if (list[idx].Rating < 1000)
    {
        list.RemoveAt(idx);
    }
}

Alle Indizes links vom entfernten Element bleiben gleich, sodass Sie keine Elemente überspringen.

Das gleiche Prinzip gilt, wenn Sie eine Liste von Indizes erhalten, die aus einem Array entfernt werden sollen. Um die Dinge gerade zu halten, müssen Sie die Liste sortieren und dann die Elemente vom höchsten zum niedrigsten Index entfernen.

Jetzt können Sie einfach Linq verwenden und auf einfache Weise deklarieren, was Sie tun.

list.RemoveAll(o => o.Rating < 1000);

In diesem Fall ist es nicht effizienter, ein einzelnes Element vorwärts oder rückwärts zu iterieren. Sie können hierfür auch Linq verwenden.

int removeIndex = list.FindIndex(o => o.Name == "Ted");
if( removeIndex != -1 )
{
    list.RemoveAt(removeIndex);
}

2
Für eine einfache Liste <T>, wenn Sie mehr als 1 Element entfernen müssen, ist die umgekehrte for-Schleife IMMER die effizienteste Methode, um dies zu tun. Dies ist mit Sicherheit effizienter als das Kopieren der Daten in eine listToRemove-Liste. Ich wette, die Linq-Implementierung verwendet denselben Trick.
Sam Saffron

@ sambo99 Ich stimme vollkommen zu und versuche, Ihre Antwort zu erweitern und zu erklären, warum die Vorwärtsiteration nicht immer funktioniert. Ich weise auch darauf hin, dass die umgekehrte Iteration ohne zusätzliche Informationen weder effizienter noch weniger effizient ist, wenn Sie höchstens einen Eintrag entfernen. O (n) ist so gut wie es mit Listen geht.
Robert Paulson

Yerp. Ich korrigiere das jetzt ... List <T> hat eine sehr unangenehme Implementierung von RemoveAt. Der effizienteste Weg scheint darin zu bestehen, die benötigten Daten aus der Liste zu kopieren
Sam Saffron,

Stellen Sie sich vor, Sie möchten, dass Ihre Daten vom linken Ende entfernt werden (sagen wir, Sie möchten die meisten linken Elemente entfernen, wissen aber nicht, wie viele). Offensichtlich ist eine umgekehrte Lösung der schlechteste Weg, dies zu tun ... In Ihrem Beispiel anstelle von WHOOPSIE! Sie sollten list.RemoveAt (idx--) geschrieben haben; Alrigth?

@ maxima120 Sie haben Recht, aber wenn Sie lesen, was ich gesagt habe, habe ich versucht zu veranschaulichen, warum die umgekehrte Iteration die meiste Zeit effizienter sein kann . Ich plädiere nicht für seine universelle Anwendbarkeit. Heutzutage würde ich mich nicht mehr selbst wiederholen und stattdessen die LINQ-Version bevorzugen.
Robert Paulson

11

Wenn es eine ist, ICollectiondann haben Sie keine RemoveAllMethode. Hier ist eine Erweiterungsmethode, die dies ermöglicht:

    public static void RemoveAll<T>(this ICollection<T> source, 
                                    Func<T, bool> predicate)
    {
        if (source == null)
            throw new ArgumentNullException("source", "source is null.");

        if (predicate == null)
            throw new ArgumentNullException("predicate", "predicate is null.");

        source.Where(predicate).ToList().ForEach(e => source.Remove(e));
    }

Basierend auf: http://phejndorf.wordpress.com/2011/03/09/a-removeall-extension-for-the-collection-class/


Vorausgesetzt, dass mehr als ein Element aus der Sammlung entfernt werden muss, ist dies der beste Weg, dies zu tun.
Xeaz

10

Für eine einfache Listenstruktur scheint der effizienteste Weg die Verwendung der Predicate RemoveAll-Implementierung zu sein.

Z.B.

 workSpace.RoleAssignments.RemoveAll(x =>x.Member.Name == shortName);

Die Gründe sind:

  1. Die Predicate / Linq RemoveAll-Methode ist in List implementiert und hat Zugriff auf das interne Array, in dem die tatsächlichen Daten gespeichert sind. Dadurch werden die Daten verschoben und die Größe des internen Arrays geändert.
  2. Die Implementierung der RemoveAt-Methode ist recht langsam und kopiert das gesamte zugrunde liegende Datenarray in ein neues Array. Dies bedeutet, dass die umgekehrte Iteration für List unbrauchbar ist

Wenn Sie dies in der Zeit vor C # 3.0 nicht weiter implementieren können. Sie haben 2 Möglichkeiten.

  • Die leicht zu wartende Option. Kopieren Sie alle übereinstimmenden Elemente in eine neue Liste und tauschen Sie die zugrunde liegende Liste aus.

Z.B.

List<int> list2 = new List<int>() ; 
foreach (int i in GetList())
{
    if (!(i % 2 == 0))
    {
        list2.Add(i);
    }
}
list2 = list2;

Oder

  • Die knifflige, etwas schnellere Option, bei der alle Daten in der Liste nach unten verschoben werden, wenn sie nicht übereinstimmen, und anschließend die Größe des Arrays geändert wird.

Wenn Sie sehr häufig Dinge aus einer Liste entfernen, ist möglicherweise eine andere Struktur wie eine HashTable (.net 1.1) oder ein Dictionary (.net 2.0) oder ein HashSet (.net 3.5) für diesen Zweck besser geeignet.


7

Welcher Typ ist die Sammlung? Wenn es List ist, können Sie das hilfreiche "RemoveAll" verwenden:

int cnt = workspace.RoleAssignments
                      .RemoveAll(spa => spa.Member.Name == shortName)

(Dies funktioniert in .NET 2.0. Wenn Sie nicht über den neueren Compiler verfügen, müssen Sie natürlich "delegate (SPRoleAssignment spa) {return spa.Member.Name == shortName;}" anstelle des nice verwenden Lambda-Syntax.)

Ein anderer Ansatz, wenn es sich nicht um eine Liste, sondern um eine ICollection handelt:

   var toRemove = workspace.RoleAssignments
                              .FirstOrDefault(spa => spa.Member.Name == shortName)
   if (toRemove != null) workspace.RoleAssignments.Remove(toRemove);

Dies erfordert die Enumerable-Erweiterungsmethoden. (Sie können die Mono-Dateien kopieren, wenn Sie mit .NET 2.0 nicht weiterkommen.) Wenn es sich um eine benutzerdefinierte Sammlung handelt, die kein Element aufnehmen kann, aber einen Index verwenden MUSS, übergeben einige der anderen Enumerable-Methoden, z. B. Select, den ganzzahligen Index für Sie.


2

Dies ist meine generische Lösung

public static IEnumerable<T> Remove<T>(this IEnumerable<T> items, Func<T, bool> match)
    {
        var list = items.ToList();
        for (int idx = 0; idx < list.Count(); idx++)
        {
            if (match(list[idx]))
            {
                list.RemoveAt(idx);
                idx--; // the list is 1 item shorter
            }
        }
        return list.AsEnumerable();
    }

Es würde viel einfacher aussehen, wenn Erweiterungsmethoden das Übergeben von Referenzen unterstützen würden! Verwendung:

var result = string[]{"mike", "john", "ali"}
result = result.Remove(x => x.Username == "mike").ToArray();
Assert.IsTrue(result.Length == 2);

BEARBEITEN: Sichergestellt, dass die Listenschleife auch beim Löschen von Elementen gültig bleibt, indem der Index (idx) dekrementiert wird.


Das ist keine funktionierende Lösung! Sie überspringen Elemente sowohl am Ende als auch in der Mitte der Liste! Versuchen Sie zum Beispiel var result = string[]{"mike", "mike", "john", "ali", "mike"}, nur das erste "Mikrofon" wird entfernt.
Flindeberg

@flindeberg Ich frage mich, was du mit "Überspringen" meinst, da die Schleife über alle Elemente läuft ?! ... aber ich werde Ihre Daten versuchen
Jalal El-Shaer

Wenn Sie Ihren Index (idx) nach dem Entfernen eines Elements erhöhen, überspringen Sie das "nächste" Element. Beispiel: Wenn Sie das Element an Position 4 entfernen , wird Position 5 auf Position 4 , Position 6 -> Position 5 , (n) -> (n - 1) usw. "verschoben" , und dann erhöhen Sie Ihren Index auf 5, wobei Sie werden finden (alte 6, neue 5) , und Sie haben übersprungen (alte 5, neue 4) . Klärt dies die Dinge?
Flindeberg

Sehr gut, ich habe meine Antwort aktualisiert ... danke, dass
du das

2

Hier ist ein ziemlich guter Weg, dies zu tun

http://support.microsoft.com/kb/555972

        System.Collections.ArrayList arr = new System.Collections.ArrayList();
        arr.Add("1");
        arr.Add("2");
        arr.Add("3");

        /*This throws an exception
        foreach (string s in arr)
        {
            arr.Remove(s);
        }
        */

        //where as this works correctly
        Console.WriteLine(arr.Count);
        foreach (string s in new System.Collections.ArrayList(arr)) 
        {
            arr.Remove(s);
        }
        Console.WriteLine(arr.Count);
        Console.ReadKey();

0

Je nachdem, wie Sie Ihre Sammlung verwenden, können Sie einen anderen Ansatz wählen. Wenn Sie die Aufgaben einmal herunterladen (z. B. wenn die App ausgeführt wird), können Sie die Sammlung im laufenden Betrieb in eine Hashtabelle übersetzen, in der:

Kurzname => SPRoleAssignment

Wenn Sie dies tun, müssen Sie beim Entfernen eines Elements mit Kurznamen lediglich das Element mit dem Schlüssel aus der Hashtabelle entfernen.

Wenn Sie diese SPRoleAssignments häufig laden, ist dies leider zeitlich nicht kosteneffizienter. Die Vorschläge anderer Leute zur Verwendung von Linq wären gut, wenn Sie eine neue Version von .NET Framework verwenden. Andernfalls müssen Sie sich an die von Ihnen verwendete Methode halten.


0

Ähnlich wie bei Dictionary Collection habe ich dies getan.

Dictionary<string, bool> sourceDict = new Dictionary<string, bool>();
sourceDict.Add("Sai", true);
sourceDict.Add("Sri", false);
sourceDict.Add("SaiSri", true);
sourceDict.Add("SaiSriMahi", true);

var itemsToDelete = sourceDict.Where(DictItem => DictItem.Value == false);

foreach (var item in itemsToDelete)
{
    sourceDict.Remove(item.Key);
}

Hinweis: Der obige Code schlägt im .Net-Client-Profil (3.5 und 4.5) fehl. Einige Betrachter haben auch erwähnt, dass er in .Net4.0 nicht funktioniert. Sie sind sich nicht sicher, welche Einstellungen das Problem verursachen.

Ersetzen Sie daher die Where-Anweisung durch den folgenden Code (.ToList ()), um diesen Fehler zu vermeiden. „Die Sammlung wurde geändert. Aufzählungsoperation wird möglicherweise nicht ausgeführt. “

var itemsToDelete = sourceDict.Where(DictItem => DictItem.Value == false).ToList();

Pro MSDN Ab .Net4.5 werden Client-Profile eingestellt. http://msdn.microsoft.com/en-us/library/cc656912(v=vs.110).aspx


0

Speichern Sie Ihre Artikel zuerst und löschen Sie sie dann.

var itemsToDelete = Items.Where(x => !!!your condition!!!).ToArray();
for (int i = 0; i < itemsToDelete.Length; ++i)
    Items.Remove(itemsToDelete[i]);

Sie müssen GetHashCode()in Ihrer Artikelklasse überschreiben .


0

Der beste Weg, dies zu tun, ist die Verwendung von linq.

Beispielklasse:

 public class Product
    {
        public string Name { get; set; }
        public string Price { get; set; }      
    }

Linq-Abfrage:

var subCollection = collection1.RemoveAll(w => collection2.Any(q => q.Name == w.Name));

Diese Abfrage wird alle Elemente entfernen , collection1wenn Namejedes Element übereinstimmen Nameauscollection2

Denken Sie daran, Folgendes zu verwenden: using System.Linq;


0

Dies ist der Ansatz, den ich in der Vergangenheit gewählt habe (beachten Sie die .ToList () am Ende der ursprünglichen Sammlung. Dadurch wird eine weitere Sammlung im Speicher erstellt , dann können Sie die vorhandene Sammlung ändern)

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments.ToList())
{
    if (spAssignment.Member.Name == shortName)
    {
        workspace.RoleAssignments.Remove(spAssignment);
    }
}

0

Wenn Sie eine haben List<T>, dann List<T>.RemoveAllist Ihre beste Wahl. Effizienter kann es nicht sein. Intern bewegt sich das Array in einem Schuss, ganz zu schweigen von O (N).

Wenn Sie nur ein IList<T>oder ein haben, haben ICollection<T>Sie ungefähr diese drei Optionen:

    public static void RemoveAll<T>(this IList<T> ilist, Predicate<T> predicate) // O(N^2)
    {
        for (var index = ilist.Count - 1; index >= 0; index--)
        {
            var item = ilist[index];
            if (predicate(item))
            {
                ilist.RemoveAt(index);
            }
        }
    }

oder

    public static void RemoveAll<T>(this ICollection<T> icollection, Predicate<T> predicate) // O(N)
    {
        var nonMatchingItems = new List<T>();

        // Move all the items that do not match to another collection.
        foreach (var item in icollection) 
        {
            if (!predicate(item))
            {
                nonMatchingItems.Add(item);
            }
        }

        // Clear the collection and then copy back the non-matched items.
        icollection.Clear();
        foreach (var item in nonMatchingItems)
        {
            icollection.Add(item);
        }
    }

oder

    public static void RemoveAll<T>(this ICollection<T> icollection, Func<T, bool> predicate) // O(N^2)
    {
        foreach (var item in icollection.Where(predicate).ToList())
        {
            icollection.Remove(item);
        }
    }

Wählen Sie entweder 1 oder 2.

1 ist leichter im Speicher und schneller, wenn Sie weniger Löschvorgänge ausführen müssen (dh das Prädikat ist meistens falsch).

2 ist schneller, wenn Sie mehr Löschvorgänge ausführen müssen.

3 ist der sauberste Code, hat aber eine schlechte IMO-Leistung. Auch hier hängt alles von den Eingabedaten ab.

Einige Details zum Benchmarking finden Sie unter https://github.com/dotnet/BenchmarkDotNet/issues/1505


0

Viele gute Antworten hier; Ich mag besonders die Lambda-Ausdrücke ... sehr sauber. Ich habe es jedoch versäumt, die Art der Sammlung nicht anzugeben. Dies ist eine SPRoleAssignmentCollection (von MOSS), die nur Remove (int) und Remove (SPPrincipal) enthält, nicht das praktische RemoveAll (). Also habe ich mich darauf geeinigt, es sei denn, es gibt einen besseren Vorschlag.

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name != shortName) continue;
    workspace.RoleAssignments.Remove((SPPrincipal)spAssignment.Member);
    break;
}

2
Mit meinem anderen Vorschlag, FirstOrDefault zu verwenden, sind Sie immer noch besser dran. things.Remove (things.FirstOrDefault (t => t.Whatever == true))
MichaelGG
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.