Интеллектуальный способ удаления элементов из списка <T>при перечислении в C # - PullRequest
80 голосов
/ 25 августа 2011

У меня есть классический случай попытки удалить элемент из коллекции при перечислении его в цикле:

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)
{
    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.
}

В начале итерации после изменения выдается InvalidOperationException, потому что перечислителям не нравится, когда изменяется базовая коллекция.

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

  1. Не удаляйте внутри этого цикла, вместо этого сохраняйте отдельный «Удалить список», который вы обрабатываете после основного цикла.

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

  2. Вместо удаления элемента просто установите флаг на элементе и отметьте его как неактивный. Затем добавьте функциональность шаблона 1, чтобы очистить список.

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

  3. Каким-то образом объединить идеи шаблона 2 в классе, производном от List<T>. Этот суперсписок будет обрабатывать флаг неактивности, удаление объектов после факта, а также не будет выставлять элементы, помеченные как неактивные для потребителей перечисления. По сути, он просто воплощает в себе все идеи шаблона 2 (и впоследствии шаблона 1).

    Существует ли такой класс? У кого-нибудь есть код для этого? Или есть лучший способ?

  4. Мне сказали, что доступ к myIntCollection.ToArray() вместо myIntCollection решит проблему и позволит мне удалить внутри цикла.

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

подробности:

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

  • Внутри цикла я буду выполнять все виды процессов, добавлять, удалять и т. Д., Поэтому решение должно быть достаточно общим.

  • Элемент, который мне нужно удалить , может не быть текущим элементом в цикле. Например, я могу быть на элементе 10 в цикле из 30 элементов и мне нужно удалить элемент 6 или элемент 26. Из-за этого больше не будет идти обратный ход по массиву. ; О (

Ответы [ 9 ]

186 голосов
/ 25 августа 2011

Лучшее решение обычно заключается в использовании метода RemoveAll():

myList.RemoveAll(x => x.SomeProp == "SomeValue");

Или, если вам нужно определенных удаленных элементов:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

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

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

Переход назад гарантирует, что вы не пропустите никаких элементов.

Ответ на редактирование :

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

List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    {
        toRemove.Add(elem);
    }
}

// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));
20 голосов
/ 25 августа 2011

Если вы оба должны перечислить List<T> и удалить из него, тогда я предлагаю просто использовать while цикл вместо foreach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
8 голосов
/ 04 апреля 2014

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

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

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}
8 голосов
/ 25 августа 2011

Если вам нужно перебрать список и изменить его во время цикла, тогда лучше использовать цикл for:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

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

Если у вас есть Linq, вы должны просто использовать RemoveAll, как предложил dlev.

5 голосов
/ 25 августа 2011

Когда вы перечисляете список, добавьте тот, который вы хотите сохранить, в новый список. После этого назначьте новый список на myIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)
{
    if (i want to delete this)
        ///
    else
        newCollection.Add(i);
}
myIntCollection = newCollection;
3 голосов
/ 10 ноября 2015

Давайте добавим вам код:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

Если вы хотите изменить список, когда вы находитесь в foreach, вы должны набрать .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}
0 голосов
/ 07 сентября 2018

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

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }
0 голосов
/ 04 ноября 2017

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

private void RemoveItems()
{
    _newList.Clear();

    foreach (var item in _list)
    {
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    }

    var swap = _list;
    _list = _newList;
    _newList = swap;
}
0 голосов
/ 25 августа 2011

Как насчет

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}
...