为什么这个方法会导致无限循环
本文关键字:无限循环 方法 为什么 | 更新日期: 2023-09-27 18:07:42
我的一位同事向我提出了一个关于这种导致无限循环的方法的问题。实际的代码有点太复杂了,不能在这里发布,但本质上问题归结为:
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
会抛出,如果它迭代的集合发生了变化。我不明白的是,因为我不是很熟悉这些东西是如何工作的,为什么重新赋值参数会导致这个无限循环。
有没有更了解内部结构的人愿意解释为什么这里会发生无限循环?
回答这个问题的关键是延迟执行。当你这样做
items = items.Select(item => items.First(i => i == item));
不迭代传递给方法的items
数组。相反,您可以为它分配一个新的IEnumerable<int>
,它将自己引用回来,并且仅在调用者开始枚举结果时才开始迭代。
这就是为什么你所有的其他修复已经处理的问题:所有你需要做的就是停止馈送IEnumerable<int>
返回给自己:
- 使用
var foo
通过使用不同的变量打破了自引用, - 使用
return items.Select...
根本不使用中间变量,从而破坏了自引用, - 使用
ToList()
通过避免延迟执行打破了自我引用:当items
被重新分配时,旧的items
已经被迭代,所以你最终得到一个普通的内存中的List<int>
。
但是如果它以自己为食,它是怎么得到任何东西的呢?
没错,它没有得到任何东西!当您尝试迭代items
并向它请求第一个项目时,延迟序列会向提供给它的序列请求处理第一个项目,这意味着序列会向自己请求处理第一个项目。此时,它一直是海龟,因为为了返回要处理的第一个项目,序列必须首先从自身获取要处理的第一个项目。
看起来Select遍历了自己的输出
你是正确的。返回一个在自身上迭代的查询。
关键是在lambda中引用items
。在查询迭代之前,items
引用不会被解析("关闭"),此时items
现在引用查询而不是源集合。这是自引用发生的地方。
想象一副牌前面有一个标记为items
的牌。现在想象一个人站在一副牌旁边,他的任务是迭代名为items
的集合。然后你把标志从甲板上移到人。当你问这个人要第一件"物品"时,他会找那一堆标有"物品"的东西——这就是他!所以他问自己第一个项目,这是循环引用发生的地方。
当您将结果赋值给新变量时,您将得到一个遍历不同集合的查询,因此不会导致无限循环。
当你调用ToList
时,你将查询添加到一个新的集合,也不会得到一个无限循环。
其他会打破循环引用的东西:
- 通过调用
ToList
在lambda
items
赋值给另一个变量并在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
传递对new items
集合的引用,这还没有实现。反过来,GetFirst
说,"嘿,我需要枚举这个集合。"这样就开始了第一次递归调用,最终导致StackOverflowException
。
Select
正在消耗原始输入。First
正在尝试使用输出
这里有很多教训要学。对我来说,最重要的是"不要修改输入参数的值"。
感谢dasblinkenlight, D Stanley和Lucas Trzesniewski的帮助。