Как условно удалить элементы из коллекции .NET - PullRequest
16 голосов
/ 17 марта 2009

Я пытаюсь написать метод расширения в .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' больше не перебирает коллекцию, почему же она не может быть изменена?

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

Спасибо.

Ответы [ 5 ]

38 голосов
/ 17 марта 2009

Для 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--;
        }
    }
}

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

7 голосов
/ 17 марта 2009

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

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

РЕДАКТИРОВАТЬ: Что касается вашего комментария в вопросе, я все еще не могу заставить его потерпеть неудачу. Вот короткий, но полный пример, который работает:

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);
    }
}

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

1 голос
/ 17 марта 2009

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

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();

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

1 голос
/ 17 марта 2009

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

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
0 голосов
/ 17 марта 2009

Еще одна версия 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);
        }
    }
}
...