迭代列表并从中删除项目的最佳方法? [关闭]

Posted

技术标签:

【中文标题】迭代列表并从中删除项目的最佳方法? [关闭]【英文标题】:Best way to iterate over a list and remove items from it? [closed] 【发布时间】:2016-08-09 23:05:08 【问题描述】:

我需要遍历 List<myObject> 并删除满足特定条件的项目。

我看到了这个答案 (https://***.com/a/1582317/5077434):

使用 for 循环反向迭代您的列表:

for (int i = safePendingList.Count - 1; i >= 0; i--)

    // some code
    // safePendingList.RemoveAt(i);

例子:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)

    if (list[i] > 5)
      list.RemoveAt(i);

list.ForEach(i => Console.WriteLine(i));

但我知道for的效率不如foreach

所以我想到了使用后者如下:

foreach (var item in myList.ToList())

    // if certain condition applies:
    myList.Remove(item)

一种方法比另一种更好吗?

编辑:

我不想使用RemoveAll(...),因为在条件之前的循环中有大量代码。

【问题讨论】:

“更好”是什么?表现?记忆?易读性? for 的效率不低于foreach。而不是循环,您可以使用RemoveAll,它具有更好的性能:msdn.microsoft.com/en-us/library/wdka673a%28v=vs.110%29.aspx 你不能从你在 foreach 中使用的同一个列表中删除项目,你会得到一个异常 (System.InvalidOperationException)。 LINQ中没有RemoveAll()这样的东西。 @poke 对于数组,这肯定不是真的,foreach 已高度优化。 【参考方案1】:

你必须循环遍历列表,for 循环是最有效的循环:

  for (int i = safePendingList.Count - 1; i >= 0; --i) 
    if (condition)
      safePendingList.RemoveAt(i);

如果要在范围中删除(不在整个列表中),只需修改for循环:

  // No Enumarable.Range(1, 10) - put them into "for"
  for (int i = Math.Min(11, safePendingList.Count - 1); i >= 1; --i)
    if (condition)
      safePendingList.RemoveAt(i); 

或者如果您必须删除 前向循环中的项目

  for (int i = 0; i < safePendingList.Count;) // notice ++i abscence
    if (condition)
      safePendingList.RemoveAt(i);
    else
      i += 1; // ++i should be here

相反,safePendingList.ToList() 创建了初始safePendingList副本,这意味着内存CPU 开销 em>:

  // safePendingList.ToList() - CPU and Memory overhead (copying)
  foreach (var item in safePendingList.ToList()) 
    if (condition)
      myList.Remove(item); // Overhead: searching
  

然而,在很多情况下,最合理的计划就是让.Net为你工作

  safePendingList.RemoveAll(item => condition);

【讨论】:

要记住的一点是,使用RemoveAt() 的效率远低于RemoveAll(),因为它会不断将索引处的所有元素打乱排列。 safePendingList.RemoveAll(item =&gt; condition); 最接近高阶 filtery 函数 除了简短易读之外,RemoveAll 的主要优点是它需要线性时间,无论需要删除多少或哪些项目。基于RemoveAt 的解决方案可能需要二次时间,因为需要重复移动列表的大部分部分。 你把我的答案吸进了你的:-\【参考方案2】:

要删除具有特定条件的项目,您可以使用

list.RemoveAll(item => item > 5);

而不是

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)

    if (list[i] > 5)
        list.RemoveAt(i);

【讨论】:

myList.Where(x =&gt; x &gt; 5) 有什么问题? @arrowd - 要获得相同的结果,它必须是 myList = myList.Where(x =&gt; x &lt;= 5).ToList(); 我不想使用 LINQ,因为条件很复杂,并且随着循环的进行而建立 @arrowd 制作列表的新副本省略某些项目与从现有列表中删除项目不同。 @arrowd 他说要使用RemoveAll(),而不是RemoveAt()【参考方案3】:

这两种方法都没有显示出显着差异。 forRemoveAll 看起来要快一些(更有可能是因为使用 Remove 您正在复制列表并且它需要重新创建它......使用值类型,这也意味着所需的内存增加了一倍)。我做了一些分析:

public static void MethodA()

    var list = new List<int>(Enumerable.Range(1, 10));
    for (int i = list.Count - 1; i >= 0; i--)
    
        if (list[i] > 5)
            list.RemoveAt(i);
    


public static void MethodB()

    var list = new List<int>(Enumerable.Range(1, 10));
    foreach (var item in list.ToList())
    
        if (item > 5)
            list.Remove(item);
    


public static void MethodC()

    var list = new List<int>(Enumerable.Range(1, 10));
    list.RemoveAll(x => x > 5);


public static void Measure(string meas, Action act)

    GC.Collect();
    GC.WaitForPendingFinalizers();
    var st = new Stopwatch();
    st.Start();
    for (var i = 0; i < 10000000; i++)
        act();
    st.Stop();
    Console.WriteLine($"meas: st.Elapsed");


static void Main(string[] args)

    Measure("A", MethodA);
    Measure("B", MethodB);
    Measure("C", MethodC);

结果:

答:00:00:04.1179163

B: 00:00:07.7417853

C: 00:00:04.3525255

其他运行会给出更相似的结果(请注意,这是针对 10 项列表中的 1000 万次迭代[因此进行 1 亿次操作]...大约 3 秒的差异并不是我所说的“显着” ,但您的里程可能会有所不同)

所以选择你认为可以更好阅读的内容(我个人会使用RemoveAll 方法)

注意:随着列表的增加(这只是 10 项),MethodB(与列表重复的 foreach)会变慢并且差异会更大,因为清单的重复成本会更高。例如,列出 1,000 个项目(条件为 500 而不是 5)和 100,000 次迭代,使MethodB 为 34 秒,而其他两个项目在我的机器上运行时间都低于 2 秒。所以是的,绝对不要使用MethodB :-)

这并不意味着foreach 的效率低于for,它只是意味着使用foreach 从列表中删除项目,您需要复制所述列表,这很昂贵。

这个其他方法(使用foreach)应该和forRemoveAll 方法一样快(在我的分析中它有点慢 - 因为它分配了一个新列表,我想 - 但很漂亮可以忽略不计):

public static void MethodD()

    var originalList = new List<int>(Enumerable.Range(1, 1000));
    var list = new List<int>(originalList.Count);
    foreach (var item in originalList)
    
        if (item <= 500)
            list.Add(item);
    
    list.Capacity = list.Count;

你只需要反转条件。

我不是建议这种方法,只是证明foreach不一定比for

【讨论】:

对,如果你想看看哪匹马更快……通常最好的策略是让 .Net 完成它的工作 - RemoveAll,+1 @DmitryBychenko 我从理论上期待这个结果,但是是的,没有什么比赛马更有趣 :-) 我毫不怀疑你知道,但由于在问题,确保相反为真的最好方法是让马跑。 10 个元素的列表对于这种测试来说太小了,因为内存分配等开销会扭曲结果。尝试一百万个元素的列表(删除所有索引小于 500,000 的项目) MethodAMethodB 之间的性能差异很可能是由于使用Remove 而不是RemoveAtRemove 必须在整个列表中搜索有问题的项目,而使用 RemoveAt,你要告诉它在哪里可以找到它。【参考方案4】:

问题是foreach 在修改集合时无法使用集合,这将导致InvalidOperationException

foreach 中使用.ToList() 可以避免这种情况,但是这会复制整个集合,这会导致在集合中循环两次(第一次是复制所有元素,第二次是过滤它)。

for 循环是更好的选择,因为我们可以通过一次收集。除此之外,您还可以使用 LINQ Where() 方法过滤集合,例如:

var myFilteredCollection = myList.Where(i => i <= 5).ToList();

如果您的条件很复杂,您可以将此逻辑移至单独的方法:

private bool IsItemOk(myObject item)

   ... your complicated logic.
   return true;

并在您的 LINQ 查询中使用它:

var myFilteredCollection = myList.Where(i => IsItemOk(i)).ToList();

其他选择是反之亦然。创建新集合并根据条件添加元素。比你可以使用foreach循环:

var newList = new List<MyObject>(myList.Count);

foreach (var item in myList)

    // if certain condition does not apply:
    newList.Add(item);

附言

但正如已经提到的.RemoveAll() 更好,因为这样您就不必“重新发明***”

【讨论】:

【参考方案5】:

另一种方法是 foreach 列表,将项目设置为 null,然后将 linq 删除为 (item => null),如前所述,您可以选择更适合您的方式

【讨论】:

以上是关于迭代列表并从中删除项目的最佳方法? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

迭代列表时更改列表的最佳方法[重复]

A *:Rust中的打开和关闭列表[关闭]

计算日期出现的次数并从中制作图表[关闭]

使用 Win32/MFC 关闭并等待子框架窗口的最佳方法

删除 jQuery UI 对话框小部件上的关闭按钮的最佳方法?

安全删除具有出色性能的数据库中的数据的最佳方法是啥? [关闭]