6

所以,这个问题只是被问到了:

如何处理“无限”的 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);
}

对于 numbers 方法,并且看起来已经为该表达式生成了一个新类型:

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

这对我来说毫无意义。在我将代码放在一起并自己执行之前,我会认为这是一个无限循环。

编辑:所以我现在明白 .Take() 可以告诉 foreach 枚举已经“结束”,而实际上它还没有,但在链接到 Take() 之前不应该完整地调用 Numbers() ? Take 结果是实际枚举的结果,对吗?但是,当 Numbers 没有完全评估时,Take 是如何执行的呢?

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

4

3 回答 3

2

这与:

  • 调用某些方法时 iEnumerable 的作用
  • 枚举的性质和Yield语句

当您枚举任何类型的 IEnumerable 时,该类会为您提供它将提供给您的下一项。它不会对所有项目做任何事情,它只是给你下一个项目。它决定了该项目将是什么。(例如,有些收藏品是有序的,有些则不是。有些不保证特定的顺序,但似乎总是按照您放入它们的顺序返回它们。)。

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

这 10 个项目中的每一个都Numbers给出下一个项目。要了解如何操作,您需要阅读 Yield 声明。它是更复杂的东西的语法糖产量非常强大。(我是一名 VB 开发人员,我很生气我仍然没有它。)它不是一个函数;它不是一个函数。这是一个有一定限制的关键字。它使定义枚举器比其他方式容易得多。

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

于 2010-04-29T19:23:05.553 回答
1

这不是无限循环的原因是您仅根据 Linq 的 Take(10) 调用的使用枚举了 10 次。现在,如果您编写如下代码:

foreach (var item in Numbers())
{
}

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

代码懒惰的原因也是代码有效的原因。本质上 Take 返回第一个项目,然后您的应用程序消费,然后它需要另一个,直到它已经采取了 10 个项目。

编辑

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

一旦你的代码达到了产量回报,你实质上是在说 Numbers() 停止执行给他们这个结果,然后当他们要求下一个项目时,在产量回报后的下一行恢复执行。

Erik Lippert 有一个关于迭代器杂项方面的精彩系列

于 2010-04-29T19:22:36.943 回答
0

基本上,您的 Numbers() 函数会创建一个枚举器。
foreach 将在每次迭代中检查枚举器是否已到达末尾,如果没有,它将继续。你的实用枚举器永远不会结束,但这没关系。这正在被懒惰地评估。
枚举器将“实时”生成结果。
这意味着如果您在其中编写 .Take(3) ,则循环将仅执行 3 次。枚举器中仍会“留下”一些项目,但不会生成它们,因为此时没有方法需要它们。
如果您尝试像函数所暗示的那样生成从 0 到无穷大的所有数字,并一次返回它们,那么这个只使用其中 10 个的程序会慢得多。这就是惰性求值的好处——从未使用过的东西永远不会被计算出来。

于 2010-04-29T19:23:46.803 回答