Интеллектуальный способ удаления элементов из List <T> при перечислении в C #

87

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

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.
}

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

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

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

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

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

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

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

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

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

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

Детали:

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

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

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

Джон Сток
источник
Возможная полезная информация для кого-то еще: ошибка Avoid Collection была изменена (инкапсуляция шаблона 1)
Джордж Дакетт,
Примечание: списки экономят много времени (обычно O (N), где N - длина списка) на перемещение значений. Если действительно необходим эффективный произвольный доступ, можно добиться удалений за O (журнал N), используя сбалансированное двоичное дерево, содержащее количество узлов в поддереве, корнем которого оно является. Это BST, ключ которого (индекс в последовательности) подразумевается.
Palec 01
См. Ответ: stackoverflow.com/questions/7193294/…
Dabbas

Ответы:

196

Лучшим решением обычно является использование 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));
dlev
источник
Что касается вашего ответа: мне нужно удалить элементы немедленно во время обработки этого элемента, а не после обработки всего цикла. Решение, которое я использую, - обнулить все элементы, которые я хочу удалить немедленно, и удалить их впоследствии. Это не идеальное решение, так как я должен везде проверять NULL, но оно ДЕЙСТВИТЕЛЬНО работает.
Джон Сток
Интересно, если 'elem' не является int, мы не можем использовать RemoveAll таким образом, чтобы он присутствовал в отредактированном коде ответа.
User M
22

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

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
ДжаредПар
источник
На мой взгляд, это должен быть принятый ответ. Это позволяет вам рассмотреть остальные элементы в вашем списке, не повторяя краткий список элементов, которые нужно удалить.
Slvrfn
13

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

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

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}
Д-Джонс
источник
Отличная идея для ".ToList ()"! Простой удар по голове, а также работает в тех случаях, когда вы напрямую не используете стандартные меты "Remove ... ()".
galaxis
1
Хотя очень неэффективно.
nawfal
8

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

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

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

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

Джастин
источник
Работает только тогда, когда вы удаляете текущий элемент. Если вы удаляете произвольный элемент, вам нужно будет проверить, был ли его индекс на / перед или после текущего индекса, чтобы решить, следует ли --i.
CompuChip 01
Исходный вопрос не дал понять, что удаление других элементов, кроме текущего, должно поддерживаться, @CompuChip. Этот ответ не изменился с момента его уточнения.
Palec 01
@Palec, я понимаю, отсюда и мой комментарий.
CompuChip 01
5

По мере того, как вы перечисляете список, добавляйте тот, который хотите СОХРАНИТЬ, в новый список. После этого назначьте новый список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

Добавим вам код:

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);
}
Кристиан Войкулеску
источник
1

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

    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;
    }
kvb
источник
0

Как насчет

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}
Олаф
источник
В текущем C # его можно переписать, как foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }для любого перечислимого, и, в List<T>частности, он поддерживает этот метод даже в .NET 2.0.
Palec 01
0

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

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

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

Таким образом, в основном "foreach" удаляет элемент из списка после его итерации.

Мой тест:

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));

list.PopForEach((item) =>
{
    Console.WriteLine($"Process {item.Name}");
});

Assert.That(list.Count, Is.EqualTo(0));

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

public static class ListExtensions
{
    public static void PopForEach<T>(this List<T> list, Action<T> action)
    {
        var index = 0;
        while (index < list.Count) {
            action(list[index]);
            list.RemoveAt(index);
        }
    }
}

Надеюсь, это может быть полезно любому.

Маркус Кнаппен Йоханссон
источник