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

352

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

Вы не можете использовать .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.

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

Теги:
generics
list
loops
key-value

22 ответа

587
Лучший ответ

Итерируйте свой список в обратном порядке с помощью цикла 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));
  • 16
    Для тех, кто приходит из Java, список C # похож на ArrayList в том смысле, что вставка / удаление - это O (n), а поиск через индекс - это O (1). Это не традиционный связанный список. Кажется немного прискорбным, что C # использует слово «список» для описания этой структуры данных, поскольку напоминает классический связанный список.
  • 60
    Ничто в названии «Список» не говорит «LinkedList». Люди, пришедшие с языков, отличных от Java, могут быть смущены, когда это будет связанный список.
Показать ещё 8 комментариев
78

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

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

  • 9
    +1: обратный путь - это ключ :)
  • 0
    это эффективно и отлично работает.
58

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

К счастью, есть более элегантное решение, чем писать цикл 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);
    }
}
  • 1
    как насчет нетривиального примера, такого как {"apple", "mango"}?
  • 6
    Это отлично сработало для меня. Простой и элегантный, и требует минимальных изменений в моем коде.
Показать ещё 5 комментариев
38
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

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

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

  • 4
    Это лучшее решение, если эффективность не имеет решающего значения.
  • 2
    это также быстрее и более читабельно: list.RemoveAll (i => true);
Показать ещё 2 комментария
21

Используя 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-го списка «хранилищ» элементов, которые вы хотите удалить, за счет копирования всего списка в массив. Во втором списке подобранных элементов, вероятно, будет меньше элементов.
19

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

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

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

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

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

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

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

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

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

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

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

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

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

list.RemoveAll(condition);

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

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

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

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

10

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

list.RemoveAll(item => item.Value == someValue);
  • 0
    Это лучшее решение, если обработка не изменяет элемент и не имеет побочных эффектов.
9
List<T> TheList = new List<T>();

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

Вы не можете использовать 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.)

4

Использование 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 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 для удаления / сохранения элементов.
2

Лучший способ удалить элементы из списка, итерации по нему - использовать 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

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

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }
  • 0
    Я высказал это мнение, так как иногда было бы более эффективно перемещаться по списку по порядку (а не в обратном порядке). Возможно, вы могли бы остановиться, когда найдете первый элемент, который нельзя удалить, потому что список упорядочен. (Вообразите разрыв, где i ++ находится в этом примере.
2

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

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

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

1
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. Выполнение этих форвардов может быть проблематичным, если размер коллекции изменяется, но назад всегда должно быть безопасно.

0

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

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

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});
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

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

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

0

Я оказался в аналогичной ситуации, когда мне пришлось удалить каждый элемент n th в заданный 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
}
-3
myList.RemoveAt(i--);

simples;
  • 1
    Что делает просто simples; делать здесь?
  • 1
    это делегат .. он отрицает мой ответ каждый раз, когда он работает

Ещё вопросы

Сообщество Overcoder
Наверх
Меню