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

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

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

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

Ответ 1

Итерируйте свой список в обратном порядке с помощью цикла 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));

Ответ 2

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

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

Ответ 3

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

К счастью, есть более элегантное решение, чем писать цикл 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);
    }
}

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

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

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

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

Ответ 7

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

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

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

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

     }

Ответ 8

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

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>, по сравнению с квадратичным временем выполнения многих других ответов.

Ответ 9

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

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

Ответ 10

List<T> TheList = new List<T>();

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

Ответ 11

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

Ответ 12

Использование 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);

Ответ 13

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

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

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

Ответ 14

Я желаю "шаблон" был примерно таким:

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

Это приведет к выравниванию кода с процессом, который продолжается в мозге программиста.

Ответ 15

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

Ответ 16

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

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

Ответ 17

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

Ответ 18

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

Ответ 19

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

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

Ответ 20

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

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

Ответ 21

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

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

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

Ответ 22

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

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

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

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

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

Ответ 23

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

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

Ответ 24

Я просто хотел добавить свои 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
}

Ответ 25

myList.RemoveAt(i--);

simples;