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

Jim*_*hel 64 c# linq stack-overflow infinite-loop

我的一位同事带着一个关于这种方法的问题来找我,导致无限循环.实际的代码有点过于介绍这里,但基本上问题归结为:

private IEnumerable<int> GoNuts(IEnumerable<int> items)
{
    items = items.Select(item => items.First(i => i == item));
    return items;
}
Run Code Online (Sandbox Code Playgroud)

应该(你会认为)只是一种非常低效的方式来创建列表的副本.我叫它:

var foo = GoNuts(new[]{1,2,3,4,5,6});
Run Code Online (Sandbox Code Playgroud)

结果是无限循环.奇怪.

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

var foo = items.Select(item => items.First(i => i == item));
return foo;
Run Code Online (Sandbox Code Playgroud)

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

更多实验表明这也有效:

items = items.Select(item => items.First(i => i == item)).ToList();
return items;
Run Code Online (Sandbox Code Playgroud)

一样简单

return items.Select(item => .....);
Run Code Online (Sandbox Code Playgroud)

好奇.

很明显,问题与重新分配参数有关,但仅限于评估延迟超出该语句.如果我添加ToList()它的工作原理.

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

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

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

das*_*ght 64

回答这个问题的关键是延迟执行.当你这样做

items = items.Select(item => items.First(i => i == item));
Run Code Online (Sandbox Code Playgroud)

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

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

  • 使用var foo不同的变量使用break自引用,
  • return items.Select...通过不使用中间变量来使用中断自引用,
  • ToList()通过避免延迟执行来使用中断自引用:在items重新分配时,old items已被迭代,因此最终得到内存中的简单内容List<int>.

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

没错,它没有得到任何东西!在您尝试迭代items并询问第一个项目的那一刻,延迟序列会询问为其提供的序列,以便处理第一个项目,这意味着序列要求自己处理第一个项目.此时,它一直是乌龟,因为为了返回第一个要处理的项目,序列必须首先从自身处理第一个项目.

  • 所以它不消耗自己的输出(即创建无限的项目序列),而是试图找到第一个迭代的项目?有意思... (3认同)

D S*_*ley 20

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

你是对的.您正在返回一个迭代自身的查询.

关键是你items 在lambda中引用.该items基准不解决("关闭了"),直到查询迭代,在这一点items现在引用查询,而不是源集合. 这就是自我引用地方.

想象一张卡牌,前面有一个标牌items.现在想象一个站在卡片组旁边的人,他的任务是迭代被称为的集合items.但是你把标志从甲板上移到了男人身上.当你问那个男人第一个"项目"时 - 他会找到标有"项目"的集合 - 现在是他!所以他问自己第一个项目,这是循环引用发生的地方.

将结果分配给变量时,您将拥有一个迭代不同集合的查询,因此不会导致无限循环.

当你调用时ToList,你将查询保存到一个新的集合,也没有得到一个无限循环.

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

  • 通过调用在lambda中保湿物品ToList
  • 分配items给另一个变量和引用拉姆达内.


Jim*_*hel 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;
    }
Run Code Online (Sandbox Code Playgroud)

如果你打电话给:

var newList = GoNuts(new[]{1, 2, 3, 4, 5, 6});
Run Code Online (Sandbox Code Playgroud)

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

Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
...
Run Code Online (Sandbox Code Playgroud)

这显示的是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;
    }
Run Code Online (Sandbox Code Playgroud)

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

有趣的是,我是正确的错误的,当我说这是消耗自己的输出.该Select被消耗原始输入,如我期望的那样.在First试图消耗输出.

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

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