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

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

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

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

используйте стандартный для-loop running назад на вашей коллекции и 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) вы можете удалить "элемент" непосредственно из "списка" без страшного "коллекция была изменена; операция перечисления может не выполняться." ошибка. Компилятор делает копию "список", так что вы можете безопасно удалить на время.

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


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


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

}

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


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

myList.RemoveAt(i--);

simples;