【问题标题】:How to conditionally remove items from a .NET collection如何有条件地从 .NET 集合中删除项目
【发布时间】:2010-10-13 19:22:27
【问题描述】:

我正在尝试在 .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);
}

这会引发相同的异常;我不确定我是否真的理解为什么“Col”不再是被迭代的集合,那么为什么不能对其进行修改?

如果有人对我如何让它发挥作用有任何建议,或者有更好的方法来实现这一点,那就太好了。

谢谢。

【问题讨论】:

  • 你在什么系列上测试这些?由于似乎只有您在第二个上遇到异常,因此您的收藏甚至可能不支持 Remove...
  • 这是一个单元测试的摘录,使用此方法时失败,其中“mockStaff”是一个预先存在的列表。员工 newStaff = 新员工{StaffId = 5};模拟员工。添加(新员工); mockStaff.RemoveWhere(s => s.StaffId == 5); //此处抛出无效操作异常
  • Lee:肯定有别的事情发生。例如,请参阅我的答案。
  • 我把方法恢复到以前的样子,现在它似乎像大家说的那样工作了。最离奇的。也许我正在失去它。感谢大家的帮助。

标签: c# .net collections extension-methods


【解决方案1】:

对于List&lt;T&gt;,它已经存在,如RemoveAll(Predicate&lt;T&gt;)。因此,我建议您保留名称(允许熟悉和优先)。

基本上,您不能在迭代时删除。有两种常见的选择:

  • 使用基于索引器的迭代 (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&lt;T&gt;

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

这种方法的优点是避免了列表的大量额外副本。

【讨论】:

  • 当我使用您的第一个常用选项从集合中删除时,我通常将 Count 存储在一个 int n 变量中,并让循环从末尾倒数到开头,而不需要 i--。在循环内保存一个 Count() 调用。 (过早的优化,我知道...)
  • @rolls 它会破坏foreach,但我们没有使用它。最常见的选项是“向后迭代”,但如果您在移除时进行补偿,向前也可以使用
【解决方案2】:

正如 Marc 所说,List&lt;T&gt;.RemoveAll() 是获取列表的方法。

我很惊讶你的第二个版本没有工作,因为你在Where() 电话之后接到了ToList() 的电话。如果没有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);
    }
}

如果你能提供一个类似的 complete 失败的例子,我相信我们可以找出原因。

【讨论】:

    【解决方案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);
                }
            }
        }
        

        【讨论】:

        • A) 您不应该将 Count 存储在外部,如果您这样做,编译器将无法删除边界检查。
        • B) 这个重载几乎没用,你必须在传入之前专门将谓词保存为 Func 否则它将满足原始的 RemoveAll(Predicate ) 而不是呼叫您的分机。
        • A) 我知道,但是即使您删除循环中的项目并依赖于计数的变化,编译器能否对其进行优化?
        • 更重要的是,由于它是一个接口,你不知道 Count 是如何实现的,也许这是对某些数据库的非常昂贵的调用,所以我不相信编译器会为我优化它。不过我同意 B。
        猜你喜欢
        • 2011-05-17
        • 2021-02-15
        • 1970-01-01
        • 1970-01-01
        • 2020-12-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-25
        相关资源
        最近更新 更多