【问题标题】:C# Modification of IEnumerable while Enumerating with ForEach使用 ForEach 枚举时 C# 修改 IEnumerable
【发布时间】:2011-03-11 04:35:01
【问题描述】:

这是我正在探索的东西,看看我是否可以接受它

List<MdiChild> openMdiChildren = new List<MdiChild>();
foreach(child in MdiManager.Pages)
{
    openMdiChildren.Add(child);
}

foreach(child in openMdiChild)
{
   child.Close();
}

并将其缩短为不需要 2 个 foreach 循环。

注意我已经更改了对象的名称以简化此示例(这些来自第 3 方控件)。但对于信息和理解 MdiManager.Pages继承了CollectionBase的形式,而IEnumerable又继承了IEnumerable

MdiChild.Close() 会从MdiManager.Pages 集合中移除打开的子节点,从而更改集合并导致如果在枚举期间修改了集合,则枚举会引发异常,例如..

foreach(child in MdiManage.Pages)
{
   child.Close();
}

我能够将foreach 工作到

((IEnumerable) MdiManager.Pages).Cast<MdiChild>.ToList()
.ForEach(new Action<MdiChild>(c => c.Close());

为什么在枚举期间修改集合没有相同的问题?我最好的猜测是,在枚举由 ToList 调用创建的 List 时,它实际上是在对 MdiManager.Pages 集合中的匹配项而不是生成的 List 执行操作。

编辑

我想明确说明我的问题是如何简化这一点,我只是想了解为什么在执行时修改集合没有问题,因为我现在已经编写了它。

【问题讨论】:

  • 我的想法是,因为它提供了一个回调,所以每个线程都可以在调用任何一个项目之前为一个项目准备一个参数(从而遍历所有项目);因此,当回调执行时,您已经迭代了可枚举。
  • @Tejs:这是不正确的;这里没有多线程。

标签: c# .net enumeration


【解决方案1】:

您对ToList() 的调用是您在这里的救命稻草,因为它实际上是在重复您在上面所做的事情。 ToList() 实际上创建了一个 List&lt;T&gt;(在这种情况下为 List&lt;MdiChild&gt;),其中包含 MdiManager.Pages 中的所有元素,然后您对 ForEach 的后续调用在 that 列表上运行,而不是在MdiManager.Pages

归根结底,这是风格偏好的问题。我个人不是 ForEach 函数的粉丝(我更喜欢 WhereToList() 这样的查询组合函数,因为它们很简单,而且它们的设计不会对原始源产生副作用,而ForEach 不是)。

你也可以这样做:

foreach(child in MdiManager.Pages.Cast<MdiChild>().ToList())
{
    child.Close();
}

从根本上说,这三种方法的作用完全相同(它们将MdiManager.Pages 的内容缓存到List&lt;MdiChild&gt;,然后遍历该缓存列表并在每个元素上调用Close()

【讨论】:

  • 是的,我只是在玩弄它。我认为这回答了最接近的问题。
  • 从概念上讲,我认为ForEach() 风格的方法被低估了,因为许多类型的集合可以相当容易地允许在这样的方法中进行修改,而在foreach 循环中更难;如果ForEach 方法传递了一个带有Current 属性的“游标”对象以及DeleteAddBeforeAddAfter 方法[游标对象可以在整个@987654343 @ 称呼]。像从列表中删除满足某些条件的所有项目这样的操作,对于上述接口可能是 O(N),但对于 IList&lt;T&gt;,它是 O(N^2)。
  • @supercat:你是如何到达 O(N^2) 的?
  • 如果使用Remove 的惯用方法实现“PurgeForAll”以在循环中取出项目,则每次删除都需要移动它之后的所有项目。写完我的评论后,我意识到可以使用IList&lt;T&gt; 读出每个项目,然后将其存储到一个新位置,但这并不能传达一个人正在做什么,以及在项目上调用Remove被删除并且对没有的项目不做任何事情(将这种处理留给底层的List 实现)。
  • IList&lt;T&gt; 的某些实现中,如果有一个包含 10,000 个项目的列表并且想要删除中间的十个项目,则调用 Remove 取出这十个项目将比阅读更快列表中的每个项目按索引并将其存储到它应该去的地方。在其他实现中,读取和重写每个项目会更快。公开PurgeForAll(Func&lt;T,Bool&gt;) 方法的实现可以以最适合其内部表示的任何方式实现它。
【解决方案2】:

当您调用ToList() 方法时,您实际上是在枚举MdiManager.Pages 并在那里创建一个List&lt;MdiChild&gt;(这就是您的foreach 循环#1)。然后,当ForEach() 方法执行时,它将枚举先前创建的List&lt;MdiChild&gt; 并对每个项目执行您的操作(这就是foreach 循环#2)。

所以本质上它是完成同样事情的另一种方式,只使用 LINQ。

【讨论】:

    【解决方案3】:

    你也可以这样写:

    foreach(var page in MdiManager.Pages.Cast<MdiChild>.ToList())
        page.Close();
    

    在任何情况下,当您在 IEnumerable 上调用 ToList() 扩展方法时;您正在创建一个全新的列表。从其源集合中删除(在本例中为 MdiManager.Pages )不会影响 ToList() 输出的列表。

    同样的技术可用于从源集合中删除元素,而不必担心影响源可枚举。

    【讨论】:

      【解决方案4】:

      你是对的。

      ToList() 创建枚举的副本,因此您正在枚举该副本。

      您也可以这样做,这是等效的,并显示您在做什么:

      var copy = new List<MdiChild>(MdiManager.Pages.Cast<MdiChild>());
      
      foreach(var child in copy)
      {
          child.Close();
      }
      

      由于您正在枚举copy 枚举的元素,因此您不必担心修改Pages 集合,因为存在于Pages 集合中的每个对象引用现在也存在于copy 中并且对Pages 的更改不会影响它。

      调用中的所有剩余方法,ForEach() 和强制转换都是多余的,可以消除。

      【讨论】:

      • 它不仅会创建一个副本,还会在 .ForEach 运行之前完成所有的复制工作。
      • 是的,这掩盖了它,这就是为什么我写的不同。
      • 你需要调用MdiManager.Pages.Cast&lt;MdiChild&gt;(),因为它只实现IEnumerable
      • @Adam - 实际上,为什么要转换为 ((IEnumerable)MdiManager.Pages) 呢?
      • @codekaizen 是否仍需要为new List(((IEnumerabe) MdiManager.Pages).Cast&lt;MdiChild&gt;,因为 List 没有采用 CollectionBase 的构造函数,或者在这种情况下甚至只有 IEnumerable。我也知道我可以做到这一点,而不必那么疯狂,只是试着去做,然后有一个问题。
      【解决方案5】:

      乍一看,罪魁祸首是ToList(),它是一个方法,将项目的副本作为List返回,从而规避了问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-28
        • 1970-01-01
        • 2021-05-09
        • 1970-01-01
        • 2023-03-26
        相关资源
        最近更新 更多