如何在C#中迭代枚举集合时修改或删除其中的项目

38

我需要从数据表中删除一些行。我听说在迭代过程中更改集合是不可取的。因此,我应该先遍历整个数据表并将所有行添加到列表中,然后遍历列表并标记要删除的行,而不是使用for循环来检查是否满足删除条件,然后标记为已删除。这样做的原因是什么?还有没有其他替代方法(而不是使用行列表)?


我编辑了标题,以便更容易找到这个问题。之前有一个重复的问题,但我可以理解用户可能错过了旧标题下的这个问题。 - Jason Jackson
8个回答

89

通过反向迭代列表听起来是更好的方法,因为如果你删除一个元素并且其他元素“填补了这个空隙”,那也没关系,因为你已经查看了那些元素。此外,您不必担心计数器变量变得大于.Count。

        List<int> test = new List<int>();
        test.Add(1);
        test.Add(2);
        test.Add(3);
        test.Add(4);
        test.Add(5);
        test.Add(6);
        test.Add(7);
        test.Add(8);
        for (int i = test.Count-1; i > -1; i--)
        {
            if(someCondition){
                test.RemoveAt(i);
            }
        }

33

借鉴 @bruno 的代码,我会反过来实现。

因为反向运动时,缺失的数组索引不会影响您循环的顺序。

var l = new List<int>(new int[] { 0, 1, 2, 3, 4, 5, 6 });

for (int i = l.Count - 1; i >= 0; i--)
    if (l[i] % 2 == 0)
        l.RemoveAt(i);

foreach (var i in l)
{
    Console.WriteLine(i);
}

但是说真的,现在我会使用LINQ:

var l = new List<int>(new int[] { 0, 1, 2, 3, 4, 5, 6 });

l.RemoveAll(n => n % 2 == 0);

9
感谢你认识到使用LINQ的力量来完成这样的事情。既然不必那么麻烦,为什么还要选择冗长的方式呢? - BenAlabaster
为什么不将列表l定义为List<int>,而是使用var,如果你知道它会是一个List<int>呢? +1 不过 - sebagomez
6
+1 给 RemoveAll。请注意,RemoveAll 不严格属于 LINQ。它适用于任何 .NET 2.0 中不支持 LINQ 的“List”类。 - Ray

18

如果您使用简单的for循环,可以从集合中删除元素。

请看以下例子:

        var l = new List<int>();

        l.Add(0);
        l.Add(1);
        l.Add(2);
        l.Add(3);
        l.Add(4);
        l.Add(5);
        l.Add(6);

        for (int i = 0; i < l.Count; i++)
        {
            if (l[i] % 2 == 0)
            {
                l.RemoveAt(i);
                i--;
            }
        }

        foreach (var i in l)
        {
            Console.WriteLine(i);
        }

4
这个方法存在缺陷,因为它并不能检查所有元素。如果你移除第i个元素,那么位于i+1的元素会变成第i个。当i被递增来进行下一次循环时,它将跳过刚刚替换掉被移除元素的位置(希望这样说能让您理解)。 - Andy Rose
1
我改正了这个。谢谢Andy的提醒。但是我的观点是你可以用for循环来修改一个集合。 - bruno conde
1
@Bruno - 绝对同意使用for循环来编辑集合,您的编辑已经解决了元素跳过的问题。 - Andy Rose
28
倒序迭代不是更好吗?使用for(int i = l.count; i > 0; i--)。这样,如果你删除了一个元素并且下一个元素“填补了它的位置”,也不会有问题,因为你已经检查过了。或者是我漏掉了什么吗? - Michael Stum
11
最好使用内置的RemoveAll方法,该方法需要一个谓词作为参数。 - ChrisW
显示剩余4条评论

5

如果您正在使用DataTable并需要能够通过表格适配器将任何更改持久化到服务器(请参见注释),这里是一个示例,说明您应该如何删除行:

DataTable dt;
// remove all rows where the last name starts with "B"
foreach (DataRow row in dt.Rows)
{
    if (row["LASTNAME"].ToString().StartsWith("B"))
    {
        // mark the row for deletion:
        row.Delete();
    }
}

调用delete方法将会把行的RowState属性设置为Deleted,但是不会从表中移除删除的行。如果在将更改持久化到服务器之前仍需要使用该表(例如,如果您想显示表的内容但不包括已删除的行),则需要在迭代每一行时检查其RowState,像这样:

foreach (DataRow row in dt.Rows)
{
    if (row.RowState != DataRowState.Deleted)
    {
        // this row has not been deleted - go ahead and show it
    }
}

从集合中删除行(就像布鲁诺的答案中所示)将会破坏表适配器,并且通常不应该在 DataTable 中这样做。


3
一个 while 循环可以处理这个问题:
int i = 0;
while(i < list.Count)
{
    if(<codition for removing element met>)
    {
        list.RemoveAt(i);
    }
    else
    {
        i++;
    }
}

这个解决方案面临与上述相同的问题,如果您删除一个项,您的索引将会失效。 - Element
2
不会的,因为只有在元素未被移除时才会增加索引。 - Andy Rose

3

如果你的目标是.NET 2.0(没有LINQ/lambda表达式),那么可以使用chakrit的解决方案,通过使用委托而不是lambda表达式:

public bool IsMatch(int item) {
    return (item % 3 == 1); // put whatever condition you want here
}
public void RemoveMatching() {
    List<int> x = new List<int>();
    x.RemoveAll(new Predicate<int>(IsMatch));
}

2

在遍历列表时进行添加或删除操作可能会破坏它,就像你所说的那样。

我通常使用双列表方法来解决这个问题:

ArrayList matches = new ArrayList();   //second list

for MyObject obj in my_list
{

    if (obj.property == value_i_care_about)
        matches.addLast(obj);
}

//now modify

for MyObject m in matches
{
    my_list.remove(m); //use second list to delete from first list
}

//finished.

0

当我需要从正在枚举的集合中移除一个项目时,我通常会反向枚举它。


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