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

我的一个同事向我提出了一个关于这个方法的问题,导致了一个无限循环。 实际的代码有点太过牵扯到这里,但基本上这个问题归结为:

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

应该 (你会认为)是一个非常低效的方式来创build一个列表的副本。 我把它叫做:

 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

我不明白,因为我不熟悉这些东西的内部工作原理,为什么重新分配参数导致这个无限循环。

有没有人有更多的内部知识谁愿意解释为什么在这里发生无限循环?

解决这个问题的关键是推迟执行 。 当你这样做

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

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

这就是为什么所有其他修复程序都处理了这个问题:您只需要停止将IEnumerable<int>给自己:

  • 使用var foo通过使用不同的variables来中断自引用,
  • 使用return items.Select...通过根本不使用中间variables来打破自引用,
  • 使用ToList()通过避免延迟执行来中断自引用:在重新分配items的时候,旧items已经被迭代,所以最终得到一个普通的内存中的List<int>

但是,如果它本身在滋养,它究竟有什么用呢?

这是正确的,它没有得到任何东西! 当您尝试迭代items并询问第一个项目时,延迟序列会询问要处理的第一个项目的序列,这意味着序列要求自己处理第一个项目。 在这一点上,它是一路下来的 ,因为为了返回第一个项目来处理序列,首先要从第一个项目处理它自己。

它看起来像select迭代它自己的输出

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

关键是你在lambda中引用itemsitems引用不会被parsing(“封闭”),直到查询迭代,此时items现在引用查询而不是源集合。 这就是自我参照地方。

在一张牌面前画一副牌,上面标有items 。 现在画一个人站在一副扑克牌的旁边,他的任务是迭代称为items的集合。 但是你把牌子从甲板上移到了那个人身上 。 当你向男人询问第一个“物品”时,他会寻找标有“物品”的物品 – 现在就是他! 所以他问自己的第一个项目,这是循环参考的地方。

当您将结果分配给variables时,您将有一个查询遍历不同的集合,因此不会导致无限循环。

当您调用ToList ,您将查询水合到一个新的集合,也不会得到一个无限循环。

其他的事情将打破循环参考:

  • 通过调用ToList lambda中的项目
  • items分配给另一个variables并在lambda内引用。

在仔细研究了两个答案后,我想出了一个更好地说明问题的小程序。

  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说,“嘿,我需要列举这个集合。” 从而开始第一个recursion调用,最终导致StackOverflowException

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

在这里可以学到很多东西。 对我而言,最重要的是“不要修改input参数的值”。

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