Как удалить элементы из общего списка во время итерации по нему?
Я ищу лучшего pattern для работы со списком элементов, каждый из которых необходимо обработать и затем в зависимости от результата удалить из списка.
вы не можете использовать .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.
есть ли элегантный способ сделать это?
22 ответов:
повторите свой список в обратном порядке с циклом 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));
простое и понятное решение:
использовать стандартный цикл for работает назад на вашей коллекции и
RemoveAt(i)удалить элементы.
обратная итерация должна быть первым, что приходит на ум, когда вы хотите удалить элементы из коллекции во время итерации по ней.
к счастью, есть более элегантное решение, чем написание цикла 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); } }
foreach (var item in list.ToList()) { list.Remove(item); }Если вы добавить ".ToList ()" в свой список (или результаты запроса LINQ), вы можете удалить "элемент" непосредственно из "списка" без страшного "коллекция была изменена; операция перечисления может не выполняться." ошибка. Компилятор делает копию "список", так что вы можете безопасно удалить на время.
во время эта модель - это не супер эффективный, он имеет естественный вид и гибкий достаточно для почти любого ситуация. Например, когда вы хотите сохранить каждый "элемент" в БД и удалить его из списка только тогда, когда сохранение БД завершится успешно.
использование ToArray () в общем списке позволяет вам сделать удаление(элемент) в вашем общем списке:
List<String> strings = new List<string>() { "a", "b", "c", "d" }; foreach (string s in strings.ToArray()) { if (s == "b") strings.Remove(s); }
выберите элементы, которые вы do хочу, а не пытаться удалить элементы не хочу. Это намного проще (и, как правило, более эффективно), чем удаление элементов.
var newSequence = (from el in list where el.Something || el.AnotherThing < 0 select el);Я хотел опубликовать это как комментарий в ответ на комментарий, оставленный Майклом Диллоном ниже, но это слишком долго и, вероятно, полезно иметь в моем ответе в любом случае:
лично я бы никогда не удалял элементы один за другим, если вам нужно удаление, то звоните
RemoveAllкоторый принимает предикат и только переставляет внутренний массив один раз, тогда какRemoveтутArray.Copyоперация для каждого удаляемого элемента.RemoveAllзначительно эффективнее.и когда вы возвращаетесь к списку, у вас уже есть индекс элемента, который вы хотите удалить, поэтому было бы гораздо эффективнее вызвать
RemoveAt, потому чтоRemoveсначала делает обход списка, чтобы найти индекс элемента, который вы пытаетесь удалить, но вы уже знаю этот индекс.так что в целом, я не вижу причин когда-либо звонить
Removeв цикле for. И в идеале, если это вообще возможно, используйте приведенный выше код для потоковой передачи элементов из списка по мере необходимости, чтобы вообще не создавать вторую структуру данных.
использование .Список() сделает копию вашего списка, как описано в этом вопросе: ToList ()-- создает ли он новый список?
С помощью ToList(), вы можете удалить из исходного списка, потому что вы на самом деле итерации над копией.
foreach (var item in listTracked.ToList()) { if (DetermineIfRequiresRemoval(item)) { listTracked.Remove(item) } }
Как любое удаление берется при условии, что вы можете использовать
list.RemoveAll(item => item.Value == someValue);
если функция, определяющая, какие элементы удалять, не имеет побочных эффектов и не мутирует элемент (это чистая функция), простое и эффективное (линейное время) решение:
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>, по сравнению с квадратичным временем выполнения многих других ответов.
List<T> TheList = new List<T>(); TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
вы не можете использовать 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--); } }обратите внимание, что в целом все эти методы зависят от поведения повторяемой коллекции. Метод, показанный здесь, будет работать со стандартным списком(Т). (Вполне возможно написать свой собственный класс коллекции и итератор, что тут разрешить удаление элементов во время цикла foreach.)
используя
RemoveилиRemoveAtв списке при повторении этого списка намеренно было сделано трудно, потому что это почти всегда неправильно. Возможно, вы сможете заставить его работать с каким-то умным трюком, но это будет очень медленно. Каждый раз, когда вы звонитеRemoveон должен просматривать весь список, чтобы найти элемент, который вы хотите удалить. Каждый раз, когда вы звонитеRemoveAtон должен переместить последующие элементы на 1 позицию влево. Как таковое, любое решение используяRemoveилиRemoveAt, потребуется квадратичное время,O (n2).использовать
RemoveAllесли вы можете. В противном случае, следующее pattern будет фильтровать список на месте в линейном времени, 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);
Я желаю "узор" был примерно такой:
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); }это приведет код в соответствие с процессом, который происходит в мозгу программиста.
предполагая, что - Это логическое свойство элемента, что, если это правда, то элемент должен быть удален:
int i = 0; while (i < list.Count()) { if (list[i].predicate == true) { list.RemoveAt(i); continue; } i++; }
Я бы переназначил список из запроса LINQ, который отфильтровал элементы, которые вы не хотели хранить.
list = list.Where(item => ...).ToList();Если список не очень велик, при этом не должно быть значительных проблем с производительностью.
лучший способ удалить элементы из списка во время итерации по нему-использовать
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; });
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); }таким образом циклы через список назад с простой старой для цикла. Делать это вперед может быть проблематично, если размер коллекция меняется, но в обратном направлении всегда должно быть безопасно.
я оказался в аналогичной ситуации, когда мне пришлось удалить каждый 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 }
стоимость удаления элемента из списка пропорциональна количеству элементов, следующих за удаляемым. В случае, когда первая половина элементов имеет право на удаление, любой подход, основанный на удалении элементов по отдельности, в конечном итоге должен будет выполнить около N*N/4 операций копирования элементов, которые могут стать очень дорогими, если список большой.
более быстрый подход заключается в сканировании списка, чтобы найти первый элемент, который будет удален (если таковой имеется), а затем из него далее скопировать каждый элемент, который должен быть сохранен в том месте, где она принадлежит. Как только это будет сделано, если элементы R должны быть сохранены, первые элементы в списке будут те элементы R и все элементы, требующие удаления, будут в конце. Если эти элементы будут удалены в обратном порядке, системе не придется копировать ни один из них, поэтому, если в списке было N элементов, из которых R элементов, включая все первые F, были сохранены, необходимо будет скопировать элементы R-F и сжать список один пункт N-R раз. Все линейное время.
мой подход заключается в том, что я сначала создаю список показателей, которые должны быть удалены. После этого я перебираю индексы и удаляю элементы из первоначального списка. Это выглядит так:
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); } }
скопируйте список, который вы повторяете. Затем удалить из копии и взаимодействовать с оригиналом. Обратное движение сбивает с толку и не работает хорошо при параллельном цикле.
var ids = new List<int> { 1, 2, 3, 4 }; var iterableIds = ids.ToList(); Parallel.ForEach(iterableIds, id => { ids.Remove(id); });
Comments