C#列表 - 在循环/迭代时删除项目

55

假设我有以下代码片段:

var data=new List<string>(){"One","Two","Three"};
for(int i=0 ; i<data.Count ; i++){
  if(data[i]=="One"){
    data.RemoveAt(i);
  }
}
下面的代码会抛出异常。
我的问题是,如何在循环时避免抛出异常并删除元素,有什么最佳方法?

2
这个对你编译通过了吗?列表上没有“Length”属性。 - Austin Salonen
请非常仔细地跟踪这段代码,并思考当您从中删除一个项目时它会真正做什么......如果接下来的项目也应该被删除,那会怎样呢? - Andrew Barber
哦,真的吗?使用foreach会抛出一个错误(在枚举集合时无法更改它)。解决这个问题的一种方法是不要枚举,而是像你在示例中所做的那样通过for循环遍历它(一旦你修复了bug data.Length --> data.Count,它就可以完美运行)。小提示:虽然它可以运行,但仍然存在缺陷,因为您不会测试所有元素。 - Eddy
如果我修正你代码中的 LenghtCount 的错误,它看起来就能正常工作了。(实际上,它确实存在一个 bug,但不应该抛出异常。) - svick
13个回答

116

如果你需要移除元素,那么必须倒序迭代列表,这样才能从列表末尾移除元素:

var data=new List<string>(){"One","Two","Three"};
for(int i=data.Count - 1; i > -1; i--)
{
    if(data[i]=="One")
    {
        data.RemoveAt(i);
    }
}

然而,使用LINQ有更高效的方法来完成这个任务(正如其他答案中所示)。


25
或者你可以向前迭代,并且如果你删除了一个元素,就不要增加计数器。但向后迭代可能更好。 - Gabe
1
我一直听说与0比较更快:for(int i=data.Count - 1; i >= 0; i--) - FocusedWolf
@FocusedWolf 只有在每次迭代都要评估Count时才需要这样做。通常,如果Count()是一个方法,只评估一次会更快,但如果它是一个字段Count,由于已经缓存,性能不会有任何差异。如果它是一个属性,则取决于属性是否访问字段(缓存,快速)或调用方法(计算,慢)。 - Dan Bechard
有更有效的方法可以使用LINQ来完成这个任务(正如其他答案所指出的)。- 有人实际上测量了其他人所指出的方法吗?我不这么认为。自从什么时候LINQ比for循环更快了?_提示:在许多情况下,SO点数并不能代表更好的知识水平_。 - Boppity Bop
对于大型列表,LINQ 可能更快,因为 RemoveAt 的时间复杂度是 O(n),其中 n 是列表的大小。因此,算法的时间复杂度为 O(n*m),其中 m 是要删除的项目数。 - kristianp

50

您可以使用 List<T>.RemoveAll 来处理此问题:

data.RemoveAll(elem => elem == "One");

顺便提一下,这只是一个 List<T> 操作。 - Chris Marisic
这很好,但我需要正常工作的算法。我明白以相反的方式循环不会影响枚举器的工作,因此它不会抛出异常。 - Ashraf Sayied-Ahmad
4
@user,你的代码示例中没有使用枚举器。 - Kirk Woll
枚举器作为一个概念。 - Ashraf Sayied-Ahmad
1
@user931589:你没有枚举 - 这不是问题。你的代码问题是越界了。你是否需要使用升序 for 循环?你的实际代码在做什么/尝试做什么? - Reed Copsey

12

您也可以使用前向循环,例如:

var data = new List<string>() { "One", "Two", "Three", "One", "One", "Four" };
for (int i = 0; i < data.Count; i++)
{
    if (data[i] == "One")
    {
        data.RemoveAt(i--);
    }
}

这一行代码 data.RemoveAt(i--); 会在迭代器变量在循环结束时失效,当列表中的项目被移除时。

它将从当前迭代值处的索引中删除项目,然后在移除项目后,迭代器将被设置为比当前值少1。在循环结束时,循环体内的增量将使其移动到下一个有效索引。

这里有一个可以工作的.NET Fiddle示例

(请注意,我个人喜欢在这种情况下使用反向循环,因为我认为它们更易于理解,这里的答案只是为了展示另一种实现方式)


1
如果“i”是第一个元素,也就是0,那么这会触发错误。 - Mert Serimer
1
@MertSerimer,你为什么认为它会抛出异常?你试过这段代码吗?上面的代码删除了第一个元素或索引为0的元素,不应该抛出异常。原因是i--将返回0,并且减量的效果将在下一次使用i时可见。在VS或链接的fiddle中尝试这段代码。 - Habib
实际上,有时候向前迭代会更好,比如你想在同一次迭代中更新所有后续元素,这样会比向后思考更自然。 - Guillaume Perrot

11

我恰巧找到了一个简单的解决方案,使用foreach.ToArray()

  var data=new List<string>(){"One","Two","Three"};
   foreach ( var d in data.ToArray()){
      if(d =="One"){
        data.Remove(d);
      }
    }

8
请注意,.toArray() 方法会创建列表的完整副本,因此在使用大型列表时,会对性能和内存消耗产生影响。 - Martin Schneider

4
您可以尝试ChrisF的反向迭代方法来删除您的项目。
您也可以简单地:
List.Remove("One");

或者:

List.RemoveAll(i => i == "One"); // removes all instances

做完就行了。遍历整个集合来删除一个单独的项目实际上没有任何意义。


3
为什么不直接减少迭代器变量呢?
var data=new List<string>(){"One","Two","Three"};
for(int i=0 ; i<data.Count ; i++){
  if(data[i]=="One"){
    data.RemoveAt(i);
    i--; // <<<<<<<<<<<
  }
}

我认为在使用空列表时,i<data.Count的计算中会出现错误。 - Greenlight
@Greenlight:在我回答的近一年后,用户@Habib给出了几乎相同的答案。还有一个人指出了关于单个元素和空列表的错误。无论是那个人还是你都没有对这个说法提供任何解释。请提供更多细节,因为我既看不到new List<string>(){"One"}的错误,也看不到new List<string>(){}的错误。 - Pewpew
我再次检查过了,我认为它是正确的。嗯,我不确定,对不起 :) - Greenlight

1
var data=new List<string>(){"One","Two","Three"};
for(int i=0; i<data.Count; ){
  if(data[i]=="One") data.RemoveAt(i);
  else ++i;
}

1
下面的通用解决方案会复制列表并处理负索引:
foreach (void item_loopVariable in MyList.ToList) {
    item = item_loopVariable;

}

1

我有一个不太光彩的技巧,想知道它会受到什么批评?

var data=new List<string>(){"One","Two","Three"};
foreach (string itm in (data.ToArray()))
{
  if string.Compare(name, "one", true) == 0) data.Remove(name);
}

我经常在C++中使用这种技巧——在改变源结构的同时对其副本进行迭代。这在关键时刻让我想起了它。 - Puppy
这种方法的行为是O(n^2)。 Remove()调用必须执行另一个线性查找以寻找要删除的对象。我不建议使用这种技术。 - Sorensen
而且,正如在早先的回复中提到的那样,它会复制每个元素。 - skst

0

我不得不从列表中删除多个项目。所以,我重新初始化了列表计数。有没有其他更好的选择?

for (int i = dtList.Count - 1; dtList.Count > 0; )
{
     DateTime tempDate = dtList[i].Item1.Date;
     var selectDates = dtList.FindAll(x => x.Item1.Date == tempDate.Date);
     selectDates.Sort((a, b) => a.Item1.CompareTo(b.Item1));
     dtFilteredList.Add(Tuple.Create(selectDates[0].Item1, selectDates[0].Item2));
     dtList.RemoveAll(x => x.Item1.Date == tempDate.Date);
     i = dtList.Count - 1;
}

你应该格式化你的答案。 - Broots Waymb

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