Использование LINQ для удаления элементов из списка <T>

656

Скажите, что у меня есть запрос LINQ, такой как:

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

Учитывая, что authorsListэто тип List<Author>, как я могу удалить Authorэлементы authorsList, которые возвращаются запросом в authors?

Или, другими словами, как я могу удалить всех равных имени Боба authorsList?

Примечание. Это упрощенный пример для целей вопроса.

ТЗ.
источник

Ответы:

1140

Ну, было бы проще исключить их в первую очередь

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

Тем не менее, это просто изменит значение authorsListвместо удаления авторов из предыдущей коллекции. В качестве альтернативы вы можете использовать RemoveAll:

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

Если вам действительно нужно сделать это на основе другой коллекции, я бы использовал HashSet, RemoveAll и Contains:

var setToRemove = new HashSet<Author>(authors);
authorsList.RemoveAll(x => setToRemove.Contains(x));
Джон Скит
источник
14
В чем причина использования HashSet для другой коллекции?
123 456 789 0
54
@LeoLuis: Это делает Containsпроверку быстрой и гарантирует, что вы оцените последовательность только один раз.
Джон Скит
2
@LeoLuis: Да, создание HashSet из последовательности оценивает его только один раз. Не уверен, что вы подразумеваете под "слабым набором".
Джон Скит
2
@ AndréChristofferAndersen: Что вы подразумеваете под "устаревшим"? Это все еще работает. Если у вас есть List<T>, это хорошо, чтобы использовать его.
Джон Скит
4
@ AndréChristofferAndersen: было бы лучше использоватьauthorsList = authorsList.Where(x => x.FirstName != "Bob")
Джон Скит
133

Было бы лучше использовать List <T> .RemoveAll для достижения этой цели.

authorsList.RemoveAll((x) => x.firstname == "Bob");
Рид Копси
источник
8
@Reed Copsey: лямбда-параметр в вашем примере заключен в скобки, т. Е. (X). Есть ли техническая причина для этого? Это считается хорошей практикой?
Мэтт Дэвис
24
Требуется с> 1 параметром. С одним параметром это необязательно, но это помогает поддерживать согласованность.
Рид Копси
48

Если вам действительно нужно удалить элементы, то как насчет Except ()?
Вы можете удалить на основе нового списка, или удалить на лету, вложив 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
источник
Except()это единственный путь в середине LINQ-оператора. IEnumerableне имеет Remove()ни RemoveAll().
Яри
29

Вы не можете сделать это со стандартными операторами LINQ, потому что LINQ обеспечивает запрос, а не поддержку обновления.

Но вы можете создать новый список и заменить старый.

var authorsList = GetAuthorList();

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

Или вы можете удалить все предметы authorsза второй проход.

var authorsList = GetAuthorList();

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

foreach (var author in authors)
{
    authorList.Remove(author);
}
Даниэль Брюкнер
источник
12
RemoveAll()не является оператором LINQ.
Даниэль Брюкнер,
Мои извинения. Вы на 100% правы. К сожалению, я не могу изменить свое отрицательное мнение. Прости за это.
Шаи Коэн
Removeтакже является методом List< T>, а не методом System.Linq.Enumerable .
DavidRR
@Daniel, поправьте меня, если я ошибаюсь, мы можем избежать .ToList () откуда условие для второго варианта. Т.е. приведенный ниже код будет работать. varhorsList = GetAuthorList (); var авторы =horsList.Where (a => a.FirstName == "Боб"); foreach (var author in авторы) {authorList.Remove (автор); }
Сай
Да, это будет работать. Превращение в список требуется только в том случае, если вам нужен список, чтобы передать его какому-либо методу, или если вы хотите добавить или удалить больше материала позже. Это также может быть полезно, если вам нужно перечислить последовательность несколько раз, потому что тогда вам нужно только один раз оценить потенциально дорогостоящее условие условия или если результат может измениться между двумя перечислениями, например, потому что условие зависит от текущего времени. Если вы хотите использовать его только в одном цикле, нет необходимости сначала сохранять результат в списке.
Даниэль Брюкнер
20

Простое решение:

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

    foreach (string s in myList)
    {
        //
    }
}
CodeLikeBeaker
источник
Как удалить "Боб" и "Джейсон" Я имею в виду несколько в списке строк?
Нео
19

Мне было интересно, есть ли разница между RemoveAllи Exceptи плюсы использования HashSet, поэтому я сделал быструю проверку производительности :)

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

Результаты ниже:

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

Как мы видим, лучшим вариантом в этом случае является использование RemoveAll(HashSet)

suszig
источник
Этот код: "l2.RemoveAll (новый HashSet <string> (toRemove) .Contains);" не должны компилироваться ... и если ваши тесты верны, то они просто вторые, что уже предложил Джон Скит.
Паскаль
2
l2.RemoveAll( new HashSet<string>( toRemove ).Contains );компилируется нормально только к вашему сведению
AzNjoE
9

Это очень старый вопрос, но я нашел очень простой способ сделать это:

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

Обратите внимание, что поскольку возвращаемая переменная authorsList- это List<T>, IEnumerable<T>возвращаемое значение Except()должно быть преобразовано в List<T>.

Карлос Мартинес Т
источник
7

Вы можете удалить двумя способами

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

или

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

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

У меня была та же проблема, если вы хотите простой вывод в зависимости от того, где вы находитесь, тогда первое решение лучше.

AsifQadri
источник
Как я могу проверить «Боб» или «Билли»?
Si8
6

Скажите, что authorsToRemoveэто IEnumerable<T>содержит элементы, которые вы хотите удалить из authorsList.

Тогда вот еще один очень простой способ выполнить задачу удаления, заданную OP:

authorsList.RemoveAll(authorsToRemove.Contains);
atconway
источник
5

Я думаю, что вы могли бы сделать что-то вроде этого

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

Хотя я думаю, что уже предоставленные решения решают проблему более понятным способом.

ebrown
источник
4

Ниже приведен пример удаления элемента из списка.

 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
Шео Дайал Сингх
источник
1

LINQ берет свое начало в функциональном программировании, которое подчеркивает неизменность объектов, поэтому не предоставляет встроенного способа обновления исходного списка на месте.

Примечание об неизменности (взято из другого ответа SO):

Вот определение неизменности из Википедии .

В объектно-ориентированном и функциональном программировании неизменный объект - это объект, состояние которого нельзя изменить после его создания.

Сэмюэл Джек
источник
0

Я думаю, вам просто нужно назначить элементы из списка авторов в новый список, чтобы применить этот эффект.

//assume oldAuthor is the old list
Author newAuthorList = (select x from oldAuthor where x.firstname!="Bob" select x).ToList();
oldAuthor = newAuthorList;
newAuthorList = null;
Aj Go
источник
0

Для обеспечения свободного хода кода (если оптимизация кода не имеет решающего значения) и вам необходимо выполнить некоторые дополнительные операции в списке:

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

или

authorsList = authorsList.Where(x => !setToRemove.Contains(x)).<do_some_further_Linq>;
Збигнев Wiadro
источник