避免 InvalidOperationException 异常的最佳实践:集合已修改?

12

经常需要这样的功能:

 foreach (Line line in lines)
 {
    if (line.FullfilsCertainConditions())
    {
       lines.Remove(line)
    }
 }

这个方法不起作用,因为我总是会得到一个 InvalidOperationException 错误,提示在循环期间 Enumerator 已经被更改。

所以我将所有这种循环都改成了以下形式:

List<Line> remove = new List<Line>();
foreach (Line line in lines)
{
   if (line.FullfilsCertainConditions())
   {
      remove.Add(line)
   }
}

foreach (Line line in remove) {
{
   lines.Remove(line);
}

我不确定这是否是最佳方法,因为在最坏的情况下,我必须在原始列表上迭代2次,所以它需要2n的时间而不是n。

有更好的方法吗?

编辑:

我能够使用Mark的答案做到这一点!但是如果我的集合没有实现RemoveAll()怎么办?

例如,一个

System.Windows.Controls.UIElementCollection

编辑2:

再次在Mark的帮助下,我现在可以调用以下内容来删除所有ScatterViewItems:

CollectionUtils.RemoveAll(manager.getWindow().IconDisplay.Items, elem => elem.GetType() == typeof(ScatterViewItem));

我之前也遇到过同样的问题,但没有找到解决方案。更糟糕的是,这不是2n而是n^2,因为lines.Remove(line)会再次迭代集合。 - Matten
但是O(2n)等于O(n) :-) 真的: 在Java中,您可以使用迭代器进行操作,或者选择一种Copy-on-Write集合实现,从而允许在迭代时进行修改。 - Waldheinz
@Marc Gravell 非常感谢!不幸的是我不能再点赞了! - anon
4个回答

17

这直接嵌入了List<T>

lines.RemoveAll(line => line.FullfilsCertainConditions());

或者在C# 2.0中:

lines.RemoveAll(delegate(Line line) {
    return line.FullfilsCertainConditions();
});
在非List<T>的情况下(根据你对问题的编辑),你可以像下面这样包装它(未经测试):
static class CollectionUtils
{
    public static void RemoveAll<T>(IList<T> list, Predicate<T> predicate)
    {
        int count = list.Count;
        while (count-- > 0)
        {
            if (predicate(list[count])) list.RemoveAt(count);
        }
    }
    public static void RemoveAll(IList list, Predicate<object> predicate)
    {
        int count = list.Count;
        while (count-- > 0)
        {
            if (predicate(list[count])) list.RemoveAt(count);
        }
    }
}

由于UIElementCollection实现了(非泛型)IList,所以这应该可以工作。而且方便的是,在C# 3.0中,您可以在IList/IList<T>之前添加this,并将其作为扩展方法使用。唯一需注意的是,匿名方法的参数将会是object,因此您需要将其强制转换。


2
@Matten - 添加了一个C# 2.0示例。 - Marc Gravell
@Marc Gravell - 非常简单,非常优雅。谢谢 :) - Matten
@Matten - .NET 2.0还是C# 2.0?.NET 2.0支持LINQ,它是C# 3.0的一部分。http://csharpindepth.com/Articles/Chapter1/Versions.aspx - Omar
1
只是为了严谨起见 - 当针对.NET 2.0时,您可以在C# 3.0中使用顶部(lambda)版本; lambda(至少在此上下文中)是一种语言特性,而不是运行时特性。 - Marc Gravell
@Omar - .NET 2.0 官方上 并不支持LINQ - 但是可以通过LinqBridge等方式进行“黑科技”实现。 - Marc Gravell
显示剩余11条评论

1

你可以直接用筛选后的列表替换原始列表:

lines = lines.Where(line => line.FullfilsCertainConditions()).ToList();

1

建立一个新列表:

public IList<Line> GetListWithoutFullfilsCertainConditions(IList<Line> fullList) 
{
    IList<Line> resultList = new List<Line>(fullList.Count);

    foreach (Line line in fullList)
    {
       if (!line.FullfilsCertainConditions())
       {
          resultList.Add(line)
       }
    }

    return resultList;
}

1

你也可以使用 while 循环。

int i = 0;
while(i < lines.Count)
{
  if (lines[i].FullfilsCertainConditions())
  {
     lines.RemoveAt(i);
  }
  else {i++;}
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接