2009-03-17 3 views
16

Я пытаюсь написать метод расширения в .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); 
} 

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

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

Спасибо.

+0

Что коллекции вы тестирования их на? Поскольку только вы, кажется, получаете исключения во втором, ваша коллекция может даже не поддерживать Remove ... –

+0

Ешьте выдержку из единичного теста, который не работает при использовании этого метода, где «mockStaff» является ранее существующим списком . Персонал newStaff = новый Персонал {StaffId = 5}; mockStaff.Add (newStaff); mockStaff.RemoveWhere (s => s.StaffId == 5); // Неверное действие, исключенное здесь. –

+0

Ли: Что-то еще должно продолжаться. См. Мой ответ для примера. –

ответ

35

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

Когда я удаляю из коллекции, используя ваш первый общий вариант, я обычно храню Count в переменной int n и позволяю циклу возвращаться с конца на начало, не нуждаясь в i--. Сохраняет вызов Count() внутри цикла. (Преждевременная оптимизация, я знаю ...) – peSHIr

+1

@rolls это сломает 'foreach', но мы не используем это. Наиболее распространенным вариантом является «итерация назад», но форварды также работают, если вы компенсируете удаление –

1

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

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 
7

Как сказал Марк, 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); 
    } 
} 

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

1

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

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

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

0

Другой вариант 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); 
     } 
    } 
} 
+0

. A) Вы не должны хранить Count вне, компилятор не может удалить проверку границ, если вы это сделаете. – Samuel

+0

B) Эта перегрузка практически бесполезна, вам нужно будет специально сохранить ваш предикат как Func , прежде чем передавать его, в противном случае он будет удовлетворять оригиналу RemoveAll (Predicate ) и не называть ваш внутренний номер. – Samuel

+0

A) Я знаю, но может ли компилятор оптимизировать его, даже когда вы удаляете элементы в цикле и зависят от изменения счетчика? – Bengt