有人可以解释这个懒惰的评估代码吗?

问题描述:

所以,这个问题只是问的SO:有人可以解释这个懒惰的评估代码吗?

How to handle an "infinite" IEnumerable?

我的示例代码:

public static void Main(string[] args) 
{ 
    foreach (var item in Numbers().Take(10)) 
     Console.WriteLine(item); 
    Console.ReadKey(); 
} 

public static IEnumerable<int> Numbers() 
{ 
    int x = 0; 
    while (true) 
     yield return x++; 
} 

是否有人可以解释为什么这是懒惰的评估?我在Reflector中查了这段代码,而且比起我开始时我更困惑。

反射器输出:

public static IEnumerable<int> Numbers() 
{ 
    return new <Numbers>d__0(-2); 
} 

对于数字方法,并且看起来已经产生了一种新型该表达式:

[DebuggerHidden] 
public <Numbers>d__0(int <>1__state) 
{ 
    this.<>1__state = <>1__state; 
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId; 
} 

这使得没有意义的我。我会认为这是一个无限循环,直到我将这些代码放在一起并自己执行它。所以我现在明白了.Take()可以告诉foreach,枚举已经“结束”,但实际上它没有,但是不应该在链接完成之前调用Numbers()转到Take()?采取的结果是什么实际上正在枚举,正确的?但是,如果Numbers没有完全评估,那么执行如何执行?

EDIT2:那么这只是'yield'关键字执行的特定编译器技巧吗?

这不是一个无限循环的原因是根据使用Linq的Take(10)调用,你只能枚举10次。现在,如果你写的代码是这样的:

foreach (var item in Numbers()) 
{ 
} 

现在这是一个无限循环,因为您的枚举器将始终返回一个新值。 C#编译器获取这些代码并将其转换为状态机。如果您的枚举器没有警卫子句来破坏执行,那么调用者必须在您的示例中执行它。

代码懒惰的原因也是代码工作的原因。从本质上讲,返回第一个项目,然后你的应用程序消耗,然后它需要另一个,直到它有10个项目。

编辑

这实际上无关与加拿的。这些被称为迭代器。 C#编译器对您的代码执行复杂的转换,从您的方法中创建一个枚举器。我建议阅读它,但基本上(这可能不是100%准确),您的代码将输入Numbers方法,您可以设想为初始化状态机。

一旦你的代码达到收益率回报,你基本上说Numbers()停止执行了,让他们回来这个结果,然后当他们要求下一个项目在收益率返回后的下一行继续执行。

Erik Lippert has a great series上迭代

+0

是的,但是为了继续进行接听呼叫,Numbers()本身不必进行全面评估吗?我知道数字本身是一个无限循环。为什么添加.Take()会突然停止Numbers的整体评估? – Tejs 2010-04-29 19:24:25

+0

有趣。感谢您的链接!这个问题(和代码)让我做了双重考虑,现在我意识到我还有更多要学习Enumerables! – Tejs 2010-04-29 19:37:40

的其它方面,这必须与:

  • 的IEnumerable做什么时,某些方法被称为
  • 枚举的性质和产量声明

当您枚举任何类型的IEnumerable时,该类将为您提供下一个它会给你的东西。它不会对全部做任何事情,它只是给你下一个项目。它决定该项目将会是什么。 (例如,某些集合是有序的,有些不是集合的,有些集合不保证特定的顺序,但似乎总是按照您放入的顺序将其返回)。

了IEnumerable扩展方法Take()将枚举10次,得到的前10个项目。你可以做Take(100000000),它会给你很多数字。但你只是在做Take(10)。它只是要求Numbers()下一个项目。 。 。 10倍。

每个那些10项,Numbers给出了一个项目。要理解如何,您需要阅读Yield语句。更复杂的是syntactic sugarYield非常强大。 (我是一个VB开发人员,非常恼火,我仍然没有它。)这不是一个函数;这是一个有一定限制的关键字。而且它使得定义一个枚举器比其他方法更容易。

其他IEnumerable扩展方法总是遍历每一个项目。调用.AsList会炸掉它。使用它大部分的LINQ查询会炸掉它。

+0

使用yield来编写自定义枚举器非常有趣。 – JoshBerke 2010-04-29 19:24:26

基本上,你的Numbers()函数创建一个枚举器。
foreach将在每次迭代中检查enumrator是否已达到结尾,如果没有,它将继续。你真正的恩将永远不会结束,但那并不重要。这是懒惰评估。
枚举器将生成​​结果“生活”。
这意味着如果你想写(3)在那里,循环只执行三次。枚举器仍然会有一些“剩余”的项目,但是它们不会生成,因为目前没有方法需要它们。
如果你想尝试从0到无穷大的所有数字,就像函数所暗示的那样,并且一次返回所有的数字,那么这个只使用10个数字的程序会慢很多。这是懒惰评估的好处 - 从未使用过的东西永远不会计算。