【问题标题】:Can someone please explain this lazy evaluation code?有人可以解释一下这个懒惰的评估代码吗?
【发布时间】:2023-04-03 04:47:01
【问题描述】:

所以,这个问题只是在 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);
}

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

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

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

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

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

【问题讨论】:

    标签: c# linq ienumerable reflector lazy-evaluation


    【解决方案1】:

    这与:

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

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

    IEnumerable 扩展方法Take() 将枚举 10 次,获取前 10 项。你可以做Take(100000000),它会给你很多数字。但你只是在做Take(10)。它只是向Numbers() 询问下一项。 . . 10 次。

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

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

    【讨论】:

    • 使用 yield 编写自定义枚举器非常有趣。
    【解决方案2】:

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

    foreach (var item in Numbers())
    {
    }
    

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

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

    编辑

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

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

    Erik Lippert has a great series 关于迭代器的其他方面

    【讨论】:

    • 是的,但是 Numbers() 本身是否必须经过全面评估才能继续执行 Take 调用?我知道 Numbers 本身就是一个无限循环。为什么添加 .Take() 会突然停止对 Numbers 进行整体评估?
    • 有趣。感谢您的链接!这个问题(和代码)让我三思而后行,现在我意识到我还有更多关于 Enumerables 的知识!
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2013-04-14
      • 2011-07-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多