Używanie LINQ do usuwania elementów z Listy <T>

654

Powiedz, że mam zapytanie LINQ, takie jak:

var authors = from x in authorsList
              where x.firstname == "Bob"
              select x;

Biorąc pod uwagę, że authorsListjest to typ List<Author>, jak mogę usunąć Authorelementy, authorsListktóre są zwracane przez zapytanie do authors?

Lub, inaczej mówiąc, jak mogę usunąć z Boba wszystkich imion równych imionom authorsList?

Uwaga: jest to uproszczony przykład do celów pytania.

TK.
źródło

Odpowiedzi:

1138

Cóż, łatwiej byłoby je wykluczyć:

authorsList = authorsList.Where(x => x.FirstName != "Bob").ToList();

Zmieniłoby to jednak wartość authorsListzamiast usuwania autorów z poprzedniej kolekcji. Alternatywnie możesz użyć RemoveAll:

authorsList.RemoveAll(x => x.FirstName == "Bob");

Jeśli naprawdę musisz to zrobić na podstawie innej kolekcji, użyłbym HashSet, RemoveAll and Contains:

var setToRemove = new HashSet<Author>(authors);
authorsList.RemoveAll(x => setToRemove.Contains(x));
Jon Skeet
źródło
14
Jaki jest powód używania HashSet do innej kolekcji?
123 456 789 0
54
@LeoLuis: Sprawia, że Containskontrola jest szybka i zapewnia, że ​​oceniasz sekwencję tylko raz.
Jon Skeet,
2
@LeoLuis: Tak, budowanie zestawu HashSet z sekwencji ocenia go tylko raz. Nie jestem pewien, co rozumiesz przez „słaby zestaw kolekcja”.
Jon Skeet,
2
@ AndréChristofferAndersen: Co rozumiesz przez „przestarzały”? Wciąż działa. Jeśli masz, możesz List<T>go użyć.
Jon Skeet
4
@ AndréChristofferAndersen: Lepiej byłoby użyćauthorsList = authorsList.Where(x => x.FirstName != "Bob")
Jon Skeet
133

Lepiej byłoby użyć Listy <T> .Usuń wszystko, aby to osiągnąć.

authorsList.RemoveAll((x) => x.firstname == "Bob");
Reed Copsey
źródło
8
@Reed Copsey: Parametr lambda w twoim przykładzie jest zamknięty w nawiasach, tj. (X). Czy jest tego techniczny powód? Czy jest to uważane za dobrą praktykę?
Matt Davis,
24
Nie. Jest to wymagane przy parametrze> 1. Z jednym parametrem jest opcjonalny, ale pomaga zachować spójność.
Reed Copsey,
48

Jeśli naprawdę musisz usunąć elementy, to co z wyjątkiem ()?
Możesz usunąć na podstawie nowej listy lub usunąć w locie, zagnieżdżając Linq.

var authorsList = new List<Author>()
{
    new Author{ Firstname = "Bob", Lastname = "Smith" },
    new Author{ Firstname = "Fred", Lastname = "Jones" },
    new Author{ Firstname = "Brian", Lastname = "Brains" },
    new Author{ Firstname = "Billy", Lastname = "TheKid" }
};

var authors = authorsList.Where(a => a.Firstname == "Bob");
authorsList = authorsList.Except(authors).ToList();
authorsList = authorsList.Except(authorsList.Where(a=>a.Firstname=="Billy")).ToList();
BlueChippy
źródło
Except()jest jedynym sposobem na przejście w środku instrukcji LINQ. IEnumerablenie ma Remove()ani RemoveAll().
Jari Turkia,
29

Nie można tego zrobić ze standardowymi operatorami LINQ, ponieważ LINQ zapewnia zapytania, a nie obsługę aktualizacji.

Ale możesz wygenerować nową listę i zastąpić starą.

var authorsList = GetAuthorList();

authorsList = authorsList.Where(a => a.FirstName != "Bob").ToList();

Lub możesz usunąć wszystkie elementy w authorsdrugim przebiegu.

var authorsList = GetAuthorList();

var authors = authorsList.Where(a => a.FirstName == "Bob").ToList();

foreach (var author in authors)
{
    authorList.Remove(author);
}
Daniel Brückner
źródło
12
RemoveAll()nie jest operatorem LINQ.
Daniel Brückner
Przepraszam. Masz 100% racji. Niestety nie mogę cofnąć mojego zdania. Przepraszam za to.
Shai Cohen
Removejest również metodą List< T>, a nie metodą System.Linq.Enumerable .
DavidRR
@Daniel, Popraw mnie, jeśli się mylę, możemy uniknąć .ToList () z Where warunek dla drugiej opcji. Tzn. Poniżej kodu będzie działać. var authorList = GetAuthorList (); var autorzy = authorList.Where (a => a.FirstName == "Bob"); foreach (autor różnych autorów) {autorList.Remove (autor); }
Sai
Tak, to zadziała. Przekształcenie go w listę jest wymagane tylko wtedy, gdy potrzebujesz listy, aby przekazać ją do jakiejś metody lub jeśli chcesz dodać lub usunąć więcej rzeczy później. Może być również przydatny, jeśli musisz wyliczyć sekwencję wiele razy, ponieważ wtedy musisz tylko ocenić potencjalnie kosztowną sytuację, w której warunek jest jeden, lub jeśli wynik może się zmienić między dwoma wyliczeniami, na przykład ponieważ warunek zależy od bieżącego czasu. Jeśli chcesz używać go tylko w jednej pętli, absolutnie nie musisz najpierw przechowywać wyniku na liście.
Daniel Brückner,
20

Proste rozwiązanie:

static void Main()
{
    List<string> myList = new List<string> { "Jason", "Bob", "Frank", "Bob" };
    myList.RemoveAll(x => x == "Bob");

    foreach (string s in myList)
    {
        //
    }
}
CodeLikeBeaker
źródło
jak usunąć „Bob” i „Jason” Mam na myśli wiele na liście ciągów?
Neo
19

Zastanawiałem się, czy jest jakaś różnica między RemoveAlli Exceptza używaniem HashSet, więc zrobiłem szybką kontrolę wydajności :)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace ListRemoveTest
{
    class Program
    {
        private static Random random = new Random( (int)DateTime.Now.Ticks );

        static void Main( string[] args )
        {
            Console.WriteLine( "Be patient, generating data..." );

            List<string> list = new List<string>();
            List<string> toRemove = new List<string>();
            for( int x=0; x < 1000000; x++ )
            {
                string randString = RandomString( random.Next( 100 ) );
                list.Add( randString );
                if( random.Next( 1000 ) == 0 )
                    toRemove.Insert( 0, randString );
            }

            List<string> l1 = new List<string>( list );
            List<string> l2 = new List<string>( list );
            List<string> l3 = new List<string>( list );
            List<string> l4 = new List<string>( list );

            Console.WriteLine( "Be patient, testing..." );

            Stopwatch sw1 = Stopwatch.StartNew();
            l1.RemoveAll( toRemove.Contains );
            sw1.Stop();

            Stopwatch sw2 = Stopwatch.StartNew();
            l2.RemoveAll( new HashSet<string>( toRemove ).Contains );
            sw2.Stop();

            Stopwatch sw3 = Stopwatch.StartNew();
            l3 = l3.Except( toRemove ).ToList();
            sw3.Stop();

            Stopwatch sw4 = Stopwatch.StartNew();
            l4 = l4.Except( new HashSet<string>( toRemove ) ).ToList();
            sw3.Stop();


            Console.WriteLine( "L1.Len = {0}, Time taken: {1}ms", l1.Count, sw1.Elapsed.TotalMilliseconds );
            Console.WriteLine( "L2.Len = {0}, Time taken: {1}ms", l1.Count, sw2.Elapsed.TotalMilliseconds );
            Console.WriteLine( "L3.Len = {0}, Time taken: {1}ms", l1.Count, sw3.Elapsed.TotalMilliseconds );
            Console.WriteLine( "L4.Len = {0}, Time taken: {1}ms", l1.Count, sw3.Elapsed.TotalMilliseconds );

            Console.ReadKey();
        }


        private static string RandomString( int size )
        {
            StringBuilder builder = new StringBuilder();
            char ch;
            for( int i = 0; i < size; i++ )
            {
                ch = Convert.ToChar( Convert.ToInt32( Math.Floor( 26 * random.NextDouble() + 65 ) ) );
                builder.Append( ch );
            }

            return builder.ToString();
        }
    }
}

Wyniki poniżej:

Be patient, generating data...
Be patient, testing...
L1.Len = 985263, Time taken: 13411.8648ms
L2.Len = 985263, Time taken: 76.4042ms
L3.Len = 985263, Time taken: 340.6933ms
L4.Len = 985263, Time taken: 340.6933ms

Jak widzimy, najlepszą opcją w tym przypadku jest użycie RemoveAll(HashSet)

suszig
źródło
Ten kod: „l2.RemoveAll (new HashSet <string> (toRemove) .Contains);” nie powinny się kompilować ... a jeśli twoje testy są poprawne, to po prostu potwierdzają to, co Jon Skeet już sugerował.
Pascal
2
l2.RemoveAll( new HashSet<string>( toRemove ).Contains );kompiluje dobrze tylko FYI
AzNjoE
9

To bardzo stare pytanie, ale znalazłem naprawdę prosty sposób:

authorsList = authorsList.Except(authors).ToList();

Zauważ, że ponieważ zmienną zwracaną authorsListjest a List<T>, IEnumerable<T>zwrócone przez Except()musi zostać przekonwertowane na a List<T>.

Carlos Martinez T.
źródło
7

Możesz usunąć na dwa sposoby

var output = from x in authorsList
             where x.firstname != "Bob"
             select x;

lub

var authors = from x in authorsList
              where x.firstname == "Bob"
              select x;

var output = from x in authorsList
             where !authors.Contains(x) 
             select x;

Miałem ten sam problem, jeśli chcesz proste wyjście w oparciu o twój warunek gdzie, to pierwsze rozwiązanie jest lepsze.

AsifQadri
źródło
Jak mogę sprawdzić „Bob” lub „Billy”?
Si8
6

Powiedzieć, że authorsToRemovejest to IEnumerable<T>, że zawiera elementy, które chcesz usunąć z authorsList.

Oto kolejny bardzo prosty sposób wykonania zadania usuwania zadanego przez OP:

authorsList.RemoveAll(authorsToRemove.Contains);
atconway
źródło
5

Myślę, że możesz zrobić coś takiego

    authorsList = (from a in authorsList
                  where !authors.Contains(a)
                  select a).ToList();

Chociaż myślę, że podane już rozwiązania rozwiązują problem w bardziej czytelny sposób.

ebrown
źródło
4

Poniżej znajduje się przykład usunięcia elementu z listy.

 List<int> items = new List<int>() { 2, 2, 3, 4, 2, 7, 3,3,3};

 var result = items.Remove(2);//Remove the first ocurence of matched elements and returns boolean value
 var result1 = items.RemoveAll(lst => lst == 3);// Remove all the matched elements and returns count of removed element
 items.RemoveAt(3);//Removes the elements at the specified index
Sheo Dayal Singh
źródło
1

LINQ ma swoje początki w programowaniu funkcjonalnym, które podkreśla niezmienność obiektów, więc nie zapewnia wbudowanego sposobu aktualizacji oryginalnej listy w miejscu.

Uwaga na temat niezmienności (zaczerpnięta z innej odpowiedzi SO):

Oto definicja niezmienności z Wikipedii .

W programowaniu obiektowym i funkcjonalnym niezmienny obiekt to obiekt, którego stanu nie można zmienić po jego utworzeniu.

Samuel Jack
źródło
0

myślę, że musisz przypisać elementy z listy autorów do nowej listy, aby uzyskać ten efekt.

//assume oldAuthor is the old list
Author newAuthorList = (select x from oldAuthor where x.firstname!="Bob" select x).ToList();
oldAuthor = newAuthorList;
newAuthorList = null;
aj idź
źródło
0

Aby zachować płynność kodu (jeśli optymalizacja kodu nie jest kluczowa) i musisz wykonać kilka dalszych operacji na liście:

authorsList = authorsList.Where(x => x.FirstName != "Bob").<do_some_further_Linq>;

lub

authorsList = authorsList.Where(x => !setToRemove.Contains(x)).<do_some_further_Linq>;
Zbigniew Wiadro
źródło