为什么这个方法会导致无限循环?
我的一个同事向我提出了一个关于这个方法的问题,导致了一个无限循环。 实际的代码有点太过牵扯到这里,但基本上这个问题归结为:
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中引用items
。 items
引用不会被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的帮助。