为什么这种方法导致无限循环?

时间:2015-08-13 16:13:34

标签: c# linq stack-overflow infinite-loop

我的一位同事带着一个关于这种方法的问题来找我,导致无限循环。实际的代码有点过于复杂,无法在此发布,但基本上问题归结为:

private IEnumerable<int> GoNuts(IEnumerable<int> items)
{
    items = items.Select(item => items.First(i => i == item));
    return items;
}

这个 (你会认为)只是一种创建列表副本的效率很低的方法。我叫它:

var foo = GoNuts(new[]{1,2,3,4,5,6});

结果是无限循环。奇怪。

我认为修改参数在风格上是一件坏事,所以我稍微修改了代码:

var foo = items.Select(item => items.First(i => i == item));
return foo;

那很有用。也就是说,该计划已经完成;没有例外。

更多实验表明这也有效:

items = items.Select(item => items.First(i => i == item)).ToList();
return items;

简单的

return items.Select(item => .....);

好奇。

很明显,问题与重新分配参数有关,但只有在评估推迟到该语句之后。如果我添加ToList()它就可以了。

对于出了什么问题,我有一个普遍的,模糊的想法。看起来Select正在迭代它自己的输出。这本身有点奇怪,因为如果它迭代的集合发生变化,通常会抛出IEnumerable

我不明白,因为我并不熟悉这些东西如何工作的内部,这就是重新分配参数导致这种无限循环的原因。

是否有人对内部人员有更多了解谁愿意解释为什么无限循环发生在这里?

3 个答案:

答案 0 :(得分:64)

回答此问题的关键是延迟执行。当你这样做时

items = items.Select(item => items.First(i => i == item));

迭代传递给方法的items数组。相反,你为它分配一个新的IEnumerable<int>,它自己引用它,并且只在调用者开始枚举结果时才开始迭代。

这就是为什么所有其他修复都解决了问题的原因:您需要做的就是停止将IEnumerable<int>反馈给自己:

  • 使用var foo使用其他变量
  • 打破自引用
  • 使用return items.Select...完全不使用中间变量来中断自引用,
  • 使用ToList()通过避免延迟执行来中断自引用:在重新分配items时,旧的items已被迭代,因此您最终得到了一个-memory List<int>
  

但如果它以自己为食,它是如何得到任何东西的呢?

没错,它没有得到任何东西!当您尝试迭代items并询问第一个项目时,延迟序列会询问为其输入的序列,以便处理第一个项目,这意味着序列要求自己处理第一个项目。此时,它是turtles all the way down,因为为了返回要处理的第一个项目,序列必须首先从自身处理第一个项目。

答案 1 :(得分:20)

  

看起来Select正在迭代自己的输出

你是对的。您将返回迭代自身的查询

关键是你在lambda 中引用items 。在查询迭代之前,items引用未解析(“关闭”),此时items现在引用查询而不是源集合。 那是发生自我引用的

想象一张卡牌,前面有一个标有items的牌子。现在想象一个站在卡片组旁边的人,他的任务是迭代名为items的集合。但是你将标志从牌组移动到 man 。当你问男人第一个“项目”时 - 他会找到标有“项目”的收藏品 - 现在是他!所以他问自己第一个项目,这是循环引用发生的地方。

当您将结果分配给 new 变量时,您会有一个迭代不同集合的查询,因此不会导致无限循环。< / p>

当您致电ToList时,您会将查询保存到新的集合中,并且不会获得无限循环。

其他会破坏循环引用的事情:

  • 通过调用ToList
  • 在lambda
    中保护项
  • items分配给另一个变量并在lambda中引用

答案 2 :(得分:5)

在研究了给出的两个答案并稍微探讨之后,我想出了一个更能说明问题的小程序。

    private int GetFirst(IEnumerable<int> items, int foo)
    {
        Console.WriteLine("GetFirst {0}", foo);
        var rslt = items.First(i => i == foo);
        Console.WriteLine("GetFirst returns {0}", rslt);
        return rslt;
    }

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(items, item);
        });
        return items;
    }

如果你打电话给:

var newList = GoNuts(new[]{1, 2, 3, 4, 5, 6});

你会反复得到这个输出,直到你最终得到StackOverflowException

Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
...

这显示的是dasblinkenlight在他更新的答案中明确指出的内容:查询进入无限循环尝试获取第一个项目。

让我们以一种稍微不同的方式写GoNuts

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        var originalItems = items;
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(originalItems, item);
        });
        return items;
    }

如果你运行它,它会成功。为什么?因为在这种情况下,对GetFirst的调用显然是传递对传递给方法的原始项的引用。在第一种情况下,GetFirst正在传递对 items集合的引用,该集合尚未实现。反过来,GetFirst说,“嘿,我需要列举这个集合。”从而开始第一次递归调用,最终导致StackOverflowException

有趣的是,当我说它消耗自己的输出时,我是对的错了。正如我所料,Select正在消耗原始输入。 First正试图消耗输出。

这里有很多经验教训。对我来说,最重要的是“不要修改输入参数的值。”

感谢dasblinkenlight,D Stanley和Lucas Trzesniewski的帮助。