【问题标题】:Modifying a collection while enumerating another one在枚举另一个集合的同时修改一个集合
【发布时间】:2013-07-25 00:00:34
【问题描述】:

我想删除所有将删除布尔值设置为 true 的硬币,并且我知道我无法从 foreach 迭代的同一个集合中删除它。所以我做了一个副本(临时),但它一直抛出同样的异常:

集合已修改;枚举操作可能无法执行。

我做错了什么?这是我的代码:

List<Coin> temp = coins;
foreach (Coin c in coins)
{
    if (c.delete)
        temp.Remove(c);
    else
        c.somethingElse();
}
coins = temp;

【问题讨论】:

    标签: c# collections xna enumeration monogame


    【解决方案1】:

    在这里做的最简单的事情就是打电话给coins.RemoveAll(coin =&gt; coin.delete);。 (谷歌List.RemoveAll)。这会给您留下一个硬币列表,在一个语句中删除了所有“删除”,无需担心计数器或循环变量或临时副本或其他任何东西。因此,您可以随心所欲地迭代剩余的硬币。

    coins.RemoveAll(coin => coin.delete);
    foreach (var coin in coins)
        coin.somethingElse();
    

    如果您需要一个只遍历列表一次的更快的实现,这就是您所需要的。不要在循环中使用带有RemoveRemoveAt 的任何解决方案,因为这些操作很慢,所以在循环中使用它们会杀死 性能。下面只会将每个“好”硬币向下移动到index 位置,然后增加index,因此任何时候所有“好”硬币都低于index。最后你删除了index 上面的所有硬币,所以你只剩下“好”的硬币了。由于它只迭代一次,它比 RemoveAll 选项快大约 2 倍,并且比循环中的任何解决方案 RemoveRemoveAt方式

    var index = 0;
    for (var i = 0; i < coins.Count; ++i)
    {
        var coin = coins[i];
        if (!coin.delete)
        {
            coin.somethingElse();
            coins[index++] = coin;
        }
    }
    coins.RemoveRange(index, coins.Count - index);
    

    【讨论】:

    • 这很了不起,但它有效吗?我的意思是,您不会对列表进行两次迭代吗? (我不知道 RemoveAll 方法在内部是如何工作的)
    • 由于集合修改限制,在使用 foreach 删除项目时,您无法真正避免 2 次迭代。使用 for 循环而不是 foreach 可能会稍微提高效率,但在大多数情况下可能不值得。可读性与过早优化。
    • @Nico 这将比一次删除每个元素要快; List.Remove 是一个相对较慢的操作,因此即使您只迭代一次,您也会多次调用该慢操作。拨打List.RemoveAll 的任何一天都会胜过。如果性能绝对是关键并且您不关心硬币的顺序,您可以使用HashSet&lt;Coin&gt; 而不是List&lt;Coin&gt;
    • 不知道这里的性能有没有这么重要,单机游戏。至少现在我会使用“for instead foreach”选项。
    • @Nico 我试过了,HashSet 无论如何都变慢了。 HashSet 可以更快地确定您的集合/列表是否包含某个元素,但插入和删除比 List 慢。
    【解决方案2】:

    我做错了什么?

    您的代码不起作用的原因是您只是将 reference 复制到 coin 变量。就像 C 中的指针一样,引用只不过是一个保存另一种类型的内存地址的变量。所以 temp 变量指向与 coin 变量完全相同的集合。

    所以我做了一个副本(临时)

    你的代码打算做什么(正如你描述的那样)是这样的:

    List<Coin> temp = coins.ToList();
    foreach (Coin c in coins)
    {
        if (c.delete)
            temp.Remove(c);
        else
            c.somethingElse();
    }
    coins = temp;
    

    (见Enumerable.ToList

    正如 Dax 已经提到的,还有其他更短的方法可以达到相同的结果。在这种情况下,List.RemoveAll 是一个完全可以接受的解决方案。您可能还想查看LINQ queries

    【讨论】:

    • 谢谢,我不知道 = 只是复制了参考,但我想这是有道理的,因为你可以重新定义它。如果有足够的代表会投票:P
    • 乐于助人。这个问题让我绊倒了好几次。您不能在 C# 中重新定义赋值运算符 (=)(有充分的理由)。 msdn.microsoft.com/en-us/library/8edha89s(v=vs.71).aspx
    【解决方案3】:

    由于您使用“xna”和“monogame”标记了您的问题,您可能会受益于避免产生垃圾的解决方案(堆内存分配会触发后续的垃圾收集器搅动)。这是我要使用的,它也适用于任何具有列表类型数据结构的编程语言,即使 LINQ 的魔力不可用:

    // Iterate the list backwards so we never skip items or accidentally
    // access past the end of the list.
    for (int i = coins.Count - 1; i >= 0; --i)
    {
       if (coins[i].delete)
       {
          coins.RemoveAt(i);
       }
       else
       {
          coins[i].somethingElse();
       }
    }
    

    在@DaxFohl 的解决方案中,我相信创建 lambda(“coin => coin.delete”)将在堆上分配垃圾内存。并且枚举也有可能从堆中分配,至少在 Mono 中是这样。我听说微软在 Windows 上的规范 .NET 实现对这些枚举进行了超级优化 w.r.t。内存使用,但我喜欢在我自己的游戏逻辑中安全地玩它。

    另一方面,如果这不是您的逻辑的内部循环样本(例如,在 Update 方法中被非常频繁地调用),那么如果这里产生了一点垃圾可能就没有那么重要了。

    【讨论】:

    • 这太酷了兄弟;是的,它在我的游戏的更新方法中被调用,这是一种处理我的玩家收集的硬币的方式。你在开发什么?
    • 一款适用于 iOS、Android、Win8、WP 和 OS X 的基于物理的游戏。因此,保持非常基础,因此在 MS .NET 和 Mono 上一切正常,对我来说很重要。祝你的项目好运!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-30
    • 2012-02-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多