Подтвердить что ты не робот

Как условно удалить элементы из коллекции .NET

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

Это была моя первая попытка:

public static void RemoveWhere<T>(this ICollection<T> Coll, Func<T, bool> Criteria){
    foreach (T obj in Coll.Where(Criteria))
        Coll.Remove(obj);
}

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

public static void RemoveWhere<T>(this ICollection<T> Coll, Func<T, bool> Criteria){
    List<T> forRemoval = Coll.Where(Criteria).ToList();

    foreach (T obj in forRemoval)
        Coll.Remove(obj);
}

Это вызывает одно и то же исключение; Я не уверен, что я действительно понимаю, почему, поскольку "Coll" больше не перебирается, поэтому почему его нельзя изменить?

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

Спасибо.

4b9b3361

Ответ 1

Для List<T> это уже существует как RemoveAll(Predicate<T>). Как таковой, я предлагаю вам сохранить имя (позволяющее ознакомиться с знакомством и приоритетом).

В принципе, вы не можете удалить во время итерации. Существует два общих варианта:

  • используйте итерацию на основе индексатора (for) и удаление
  • буферизировать элементы для удаления и удалить после foreach (как вы уже сделали)

Итак, возможно:

public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate) {
    for (int i = 0; i < list.Count; i++) {
        if (predicate(list[i])) {
            list.RemoveAt(i--);
        }
    }
}

Или, как правило, для любого ICollection<T>:

public static void RemoveAll<T>(this ICollection<T> collection, Func<T, bool> predicate) {
    T element;

    for (int i = 0; i < collection.Count; i++) {
        element = collection.ElementAt(i);
        if (predicate(element)) {
            collection.Remove(element);
            i--;
        }
    }
}

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

Ответ 2

Как сказал Марк, List<T>.RemoveAll() - это путь для списков.

Я удивлен, что ваша вторая версия не работает, хотя, учитывая, что вы получили вызов ToList() после вызова Where(). Без вызова ToList() это, безусловно, имеет смысл (потому что это будет оценено лениво), но это должно быть хорошо, как есть. Не могли бы вы показать короткий, но полный пример этого отказа?

EDIT: Что касается вашего комментария в вопросе, я все равно не могу заставить его потерпеть неудачу. Вот короткий, но полный пример, который работает:

using System;
using System.Collections.Generic;
using System.Linq;

public class Staff
{
    public int StaffId;
}

public static class Extensions
{
    public static void RemoveWhere<T>(this ICollection<T> Coll,
                                      Func<T, bool> Criteria)
    {
        List<T> forRemoval = Coll.Where(Criteria).ToList();

        foreach (T obj in forRemoval)
        {
            Coll.Remove(obj);
        }
    }
}

class Test
{
    static void Main(string[] args)
    {
        List<Staff> mockStaff = new List<Staff>
        {
            new Staff { StaffId = 3 },
            new Staff { StaffId = 7 }
        };

       Staff newStaff = new Staff{StaffId = 5};
       mockStaff.Add(newStaff);
       mockStaff.RemoveWhere(s => s.StaffId == 5);

       Console.WriteLine(mockStaff.Count);
    }
}

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

Ответ 3

Я только что протестировал его, и ваш второй метод работает нормально (как и должно быть). Что-то еще должно идти не так, можете ли вы предоставить несколько примеров кода, который показывает проблему?

List<int> ints = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

ints.RemoveWhere(i => i > 5);
foreach (int i in ints)
{
    Console.WriteLine(i);
}

Получает:

1
2
3
4
5

Ответ 4

Я просто попробовал ваш второй пример и, похоже, работает нормально:

Collection<int> col = new Collection<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
col.RemoveWhere(x => x % 2 != 0);

foreach (var x in col)
    Console.WriteLine(x);
Console.ReadLine();

Я не получил исключения.

Ответ 5

Другая версия Marcs RemoveAll:

public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate)
{
    int count = list.Count;
    for (int i = count-1; i > -1; i--)
    {
        if (predicate(list[i]))
        {
            list.RemoveAt(i);
        }
    }
}