Как удалить элементы из общего списка при переборах по нему?


451

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

Вы не можете использовать .Remove(element)внутри foreach (var element in X)(потому что это приводит к Collection was modified; enumeration operation may not execute.исключению) ... вы также не можете использовать for (int i = 0; i < elements.Count(); i++)и .RemoveAt(i)потому, что это разрушает вашу текущую позицию в коллекции относительно i.

Есть ли элегантный способ сделать это?

Ответы:


735

Итерируйте свой список в обратном порядке с помощью цикла for:

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

Пример:

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

Кроме того, вы можете использовать метод RemoveAll с предикатом для проверки:

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

Вот упрощенный пример для демонстрации:

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

19
Для тех, кто приходит из Java, список C # похож на ArrayList в том смысле, что вставка / удаление - это O (n), а поиск через индекс - это O (1). Это не традиционный связанный список. Кажется немного прискорбным, что C # использует слово «Список» для описания этой структуры данных, поскольку напоминает классический связанный список.
Джаррод Смит

80
Ничто в названии «Список» не говорит «LinkedList». Люди, пришедшие с языков, отличных от Java, могут быть сбиты с толку, если это будет связанный список.
GolezTrol

5
Я попал сюда через поиск по vb.net, так что в случае, если кто-то захочет получить эквивалентный синтаксис vb.net для RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
псевдокодер

1
Это также работает в Java с минимальной модификацией: .size()вместо .Countи .remove(i)вместо .removeAt(i). Clever - спасибо!
Осень Леонард

2
Я провел небольшой тест на производительность, и оказалось, что он RemoveAll()занимает в три раза больше времени, чем обратный forцикл. Так что я определенно придерживаюсь цикла, по крайней мере, в тех разделах, где это важно.
Crusha K. Rool

84

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

Используйте стандартный цикл for, работающий задом наперед на вашей коллекции и RemoveAt(i)удаляющий элементы.


2
Имейте в виду , что удаление отдельных записей время не эффективно , если ваш список содержит много элементов. У него есть потенциал быть O (n ^ 2). Представьте себе список с двумя миллиардами элементов, где просто так получается, что первый миллиард элементов в итоге удаляется. Каждое удаление вынуждает копировать все последующие элементы, так что в итоге вы копируете миллиард элементов каждый миллиард раз. Это не из-за обратной итерации, а из-за удаления по одному. RemoveAll гарантирует, что каждый элемент копируется не более одного раза, поэтому он является линейным. Одновременное удаление может быть в миллиард раз медленнее. O (n) против O (n ^ 2).
Брюс Доусон

71

Обратная итерация должна быть первой вещью, которая приходит на ум, когда вы хотите удалить элементы из Коллекции во время итерации по ней.

К счастью, есть более элегантное решение, чем написание цикла for, которое включает в себя ненужную типизацию и может быть подвержено ошибкам.

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

7
Это отлично сработало для меня. Простой и элегантный, и требует минимальных изменений в моем коде.
Стивен МакДугалл

1
Это просто гений флиппин? И я согласен с @StephenMacDougall, мне не нужно использовать эти C ++ 'y для циклов и просто продолжать работать с LINQ, как и предполагалось.
Петр Кула

5
Я не вижу никаких преимуществ перед простым foreach (int myInt в test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Вы все еще должны выделить копию для Reverse, и это вводит Ха? момент - почему есть обратное.
Джахав

11
@jedesah Да, Reverse<T>()создает итератор, который просматривает список в обратном направлении, но выделяет для него дополнительный буфер того же размера, что и сам список ( referenceource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>не просто просматривать исходный список в обратном порядке (без выделения дополнительной памяти). Поэтому оба ToList()и Reverse()имеют одинаковое потребление памяти (оба создают копию), но ToList()ничего не делают с данными. С Reverse<int>(), я хотел бы знать, почему список перевернут, по какой причине.
Яхав

1
@jahav Я понимаю твою точку зрения. Это очень разочаровывает, что реализация Reverse<T>()создает новый буфер, я не совсем уверен, что понимаю, почему это необходимо. Мне кажется, что в зависимости от базовой структуры Enumerable, по крайней мере, в некоторых случаях должно быть возможно выполнить обратную итерацию без выделения линейной памяти.
Jedesah

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

Если вы добавите «.ToList ()» в свой список (или результаты запроса LINQ), вы сможете удалить «элемент» непосредственно из «списка» без изменения «ужасной» коллекции; операция перечисления может не выполняться . » ошибка. Компилятор создает копию «списка», чтобы вы могли безопасно выполнять удаление массива.

Хотя этот шаблон не очень эффективен, он имеет естественный вид и достаточно гибок практически для любой ситуации . Например, когда вы хотите сохранить каждый «элемент» в БД и удалить его из списка только тогда, когда сохранение БД выполнено успешно.


6
Это лучшее решение, если эффективность не имеет решающего значения.
IvanP

2
это также быстрее и более читабельно: list.RemoveAll (i => true);
Сантьяго Аристи

1
@ Грег Литтл, правильно ли я вас понял - когда вы добавляете ToList (), компилятор просматривает скопированную коллекцию, но удаляет ее из оригинала?
Pyrejkee

Если ваш список содержит повторяющиеся элементы, и вы хотите удалить только элемент, встречающийся позже в списке, то не удалит ли это неправильный элемент?
Тим МБ

Это также работает для списка объектов?
Том эль Сафади

23

Выберите элементы, которые вы делаете хотите, вместо того, чтобы пытаться удалить элементы, которые вы не хотите. Это намного проще (и вообще более эффективно), чем удаление элементов.

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

Я хотел опубликовать это как комментарий в ответ на комментарий, оставленный Майклом Диллоном ниже, но это слишком долго и, вероятно, полезно в моем ответе в любом случае:

Лично я никогда не удаляю элементы по одному, если вам нужно удалить, а затем позвонить RemoveAll который принимает предикат и только один раз переставляет внутренний массив, тогда как Removeвыполняет Array.Copyоперацию для каждого удаляемого элемента. RemoveAllзначительно эффективнее

И когда вы перебираете список в обратном направлении, у вас уже есть индекс элемента, который вы хотите удалить, поэтому было бы гораздо эффективнее вызвать его RemoveAt, потому что Removeсначала выполняется обход списка, чтобы найти индекс элемента, который вы хотите удалить. Пытаюсь удалить, но вы уже знаете этот индекс.

Итак, в общем, я не вижу никакой причины когда-либо вызывать Removeцикл for. И в идеале, если это вообще возможно, используйте приведенный выше код для потоковой передачи элементов из списка по мере необходимости, чтобы вообще не нужно было создавать вторую структуру данных.


1
Либо так, либо добавьте указатель на нежелательные элементы во второй список, затем, после окончания цикла, выполните итерацию списка удаления и используйте его для удаления элементов.
Майкл Диллон

22

Использование ToArray () в общем списке позволяет вам удалить (элемент) из общего списка:

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

2
Это не так, но я должен отметить, что это обходит необходимость создания 2-го списка «хранилищ» элементов, которые вы хотите удалить, за счет копирования всего списка в массив. Во втором списке отобранных элементов, вероятно, будет меньше элементов.
Ахмад Магид

20

Использование .ToList () создаст копию вашего списка, как объясняется в этом вопросе: ToList () - Создает ли он новый список?

Используя ToList (), вы можете удалить из исходного списка, потому что вы на самом деле перебираете копию.

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

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

     }

1
Но с точки зрения производительности вы копируете свой список, что может занять некоторое время. Хороший и простой способ сделать это, но с не очень хорошим исполнением
Florian K

12

Если функция, определяющая, какие элементы для удаления, не имеет побочных эффектов и не мутирует элемент (это чистая функция), простое и эффективное решение (с линейным временем):

list.RemoveAll(condition);

Если есть побочные эффекты, я бы использовал что-то вроде:

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

Это все еще линейное время, при условии, что хеш хорош. Но он имеет увеличенное использование памяти из-за хэш-набора.

Наконец, если ваш список - только IList<T>вместо, List<T>я предлагаю мой ответ на Как я могу сделать этот специальный итератор foreach? , Это будет иметь линейное время выполнения с учетом типичных реализаций IList<T>, по сравнению с квадратичным временем выполнения многих других ответов.


11

Поскольку любое удаление берется при условии, что вы можете использовать

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

Это лучшее решение, если обработка не изменяет элемент и не имеет побочных эффектов.
CodesInChaos


8

Вы не можете использовать foreach, но вы можете выполнять итерацию вперед и управлять переменной индекса цикла при удалении элемента, например, так:

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

Обратите внимание, что в целом все эти методы основаны на поведении итерируемой коллекции. Техника, показанная здесь, будет работать со стандартным списком (T). (Вполне возможно написать свой собственный класс коллекции и итератор, который позволяет удалять элементы во время цикла foreach.)


6

Использование Removeили RemoveAtв списке во время итерации по этому списку было намеренно затруднено, потому что это почти всегда неправильно . Возможно, вам удастся заставить его работать с какой-то хитрой уловкой, но это будет очень медленно. Каждый раз, когда вы звоните, Removeон должен просмотреть весь список, чтобы найти элемент, который вы хотите удалить. Каждый раз, когда вы звоните, RemoveAtон должен перемещать последующие элементы на 1 позицию влево. Таким образом, любое решение, использующее Removeили RemoveAt, потребовало бы квадратичного времени O (n²) .

Используйте, RemoveAllесли можете. В противном случае следующий шаблон отфильтрует список на месте за линейное время 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);

3

Я бы хотел, чтобы «шаблон» был примерно таким:

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

Это позволит привести код в соответствие с процессом, который происходит в мозгу программиста.


1
Достаточно просто. Просто создайте массив логических флагов (используйте тип с 3 состояниями, например Nullable<bool>, если вы хотите разрешить немаркированные), а затем используйте его после foreach для удаления / сохранения элементов.
Дэн Бешард

3

Предполагая, что предикат является логическим свойством элемента, что, если оно истинно, элемент должен быть удален:

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

Я высказал это мнение, поскольку иногда было бы более эффективно перемещаться по списку по порядку (а не в обратном порядке). Возможно, вы могли бы остановиться, когда найдете первый элемент, который нельзя удалить, поскольку список упорядочен. (Вообразите «разрыв», где i ++ находится в этом примере.
FrankKrumnow

3

Я бы переназначил список из запроса LINQ, который отфильтровал элементы, которые вы не хотели сохранять.

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

Если список не очень большой, при этом не должно быть серьезных проблем с производительностью.


3

Лучший способ удалить элементы из списка во время его перебора - использовать RemoveAll() . Но основная проблема, которую пишут люди, заключается в том, что они должны делать некоторые сложные вещи внутри цикла и / или иметь сложные случаи сравнения.

Решение по-прежнему использовать, RemoveAll()но использовать эту запись:

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

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

{

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

}

Просто создайте совершенно новый список из первого. Я говорю «Легко», а не «Правильно», поскольку создание совершенно нового списка, вероятно, приводит к выигрышу в производительности по сравнению с предыдущим методом (я не удосужился провести сравнительный анализ). Я обычно предпочитаю этот шаблон, он также может быть полезен при преодолении Ограничения Linq-To-Entities.

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

{

item=list[i];

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

}

Этот способ циклически перебирает список в обратном направлении с помощью простого старого цикла For. Делать это вперед может быть проблематично, если размер коллекции изменяется, но в обратном направлении всегда должно быть безопасно.


1

Скопируйте список, который вы повторяете. Затем удалите из копии и внесите оригинал. Возвращение назад сбивает с толку и не работает хорошо при параллельном цикле.

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

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

1

Я бы так сделал

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

ВЫВОД

Fred Jones
Billy TheKid

0

Я оказался в похожей ситуации, когда мне пришлось удалить каждый n- й элемент в заданном 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

Стоимость удаления элемента из списка пропорциональна количеству элементов, следующих за одним, подлежащим удалению. В случае, когда первая половина элементов соответствует критериям удаления, любой подход, основанный на удалении элементов по отдельности, в конечном итоге должен будет выполнять около N * N / 4 операций копирования элементов, что может оказаться очень дорогим, если список большой ,

Более быстрый подход заключается в сканировании списка, чтобы найти первый удаляемый элемент (если таковой имеется), а затем с этого момента копировать каждый элемент, который должен быть сохранен в том месте, где он принадлежит. Как только это будет сделано, если R элементов должны быть сохранены, первыми элементами R в списке будут эти элементы R, и все элементы, требующие удаления, будут в конце. Если эти элементы будут удалены в обратном порядке, системе не придется копировать какой-либо из них, поэтому, если в списке было N элементов, из которых R элементов, включая все первые F, были сохранены, необходимо будет скопируйте элементы RF и сократите список на один элемент NR раз. Все линейное время.


0

Мой подход заключается в том, что я сначала создаю список индексов, которые должны быть удалены. После этого я перебираю индексы и удаляю элементы из первоначального списка. Это выглядит так:

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

В C # один простой способ - пометить те, которые вы хотите удалить, а затем создать новый список для повторения ...

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

или еще проще использовать linq ....

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

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


0

Проследите элементы, которые должны быть удалены со свойством, и удалите их все после процесса.

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);

0

Я просто хотел добавить свои 2 цента к этому на случай, если это кому-нибудь поможет, у меня была похожая проблема, но мне нужно было удалить несколько элементов из списка массивов, пока он повторялся. ответ с наибольшим числом голосов делал это для меня по большей части, пока я не столкнулся с ошибками и не понял, что в некоторых случаях индекс превышал размер списка массивов, поскольку удалялось несколько элементов, но индекс цикла не сохранялся отслеживать это. Я исправил это с помощью простой проверки:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}

-4
myList.RemoveAt(i--);

simples;

1
Что simples;здесь делать?
Денни

6
это делегат .. он отрицает мой ответ при каждом запуске
SW

SW ROFL! Должен любить ваш комментарий.
Эрик Браун
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.