【问题标题】:Yield statement's effect on program flowYield 语句对程序流的影响
【发布时间】:2013-05-24 06:32:30
【问题描述】:

我试图了解在 C# 中使用 yield 关键字,因为我正在使用的队列建模包广泛使用它。

为了演示yield的使用,我正在使用以下代码:

using System;
using System.Collections.Generic;
public class YieldTest
{
    static void Main()
    {
        foreach (int value in ComputePower(2, 5))
        {
            Console.Write(value);
            Console.Write(" ");
        }
        Console.WriteLine();
    }
    /**
     * Returns an IEnumerable iterator of ints
     * suitable for use in a foreach statement
     */
    public static IEnumerable<int> ComputePower(int number, int exponent)
    {
        Console.Write ("Arguments to ComputePower are number: " + number + " exponent: " + exponent + "\n");
        int exponentNum = 0;
        int numberResult = 1;
        while (exponentNum < exponent)
        {
            numberResult *= number;
            exponentNum++;
            // yield: 
            // a) returns back to the calling function (foreach),
            // b) updates iterator value (2,4,8,16,32 etc.)
            yield return numberResult;
        }
    }
}

代码的作用非常明显,它只是使用 ComputePower 将 2 提高到一个幂,返回一个 IEnumerable。在调试代码时,我看到yield 语句将控制权返回给foreach 循环,并且value 变量更新为最新的power ie 结果。 2、4、8、16、32。

不完全理解yield 的使用,我预计ComputePower 会在值迭代ComputePower 时被多次调用,并且我会看到"Arguments to ComputePower are " 等控制台写入发生5 次。但实际发生的情况是,ComputePower 方法似乎只调用了一次。我每次运行只看到一次"Arguments to ComputePower.." 字符串。

有人可以解释为什么会这样吗?跟yield关键字有关系吗?

【问题讨论】:

  • 你读过yield吗?
  • 您应该能够在调试器中单步执行它以查看确切的流程。
  • 是的,或者 Jon Skeet 的 C# in Depth 很好地解释了这一点
  • 另外,this answer 提供了一些关于该主题的最佳资源的链接

标签: c# yield-return


【解决方案1】:

foreach 将迭代从 ComputePower 返回的 IEnumerable。 “收益回报”自动创建 IEnumerable 的实现,因此您不必手动滚动它。如果你在“while”循环中放置一个断点,你会看到每次迭代都会调用它

来自 msdn:

您通过使用 foreach 语句或 LINQ 查询来使用迭代器方法。 foreach 循环的每次迭代都会调用迭代器方法。在迭代器方法中到达 yield return 语句时,返回表达式,并保留代码中的当前位置。下次调用迭代器函数时,将从该位置重新开始执行。

【讨论】:

  • 关键部分是“下次调用迭代器函数时从该位置重新启动执行”。
  • 我认为 JohnDRoadch 是正确的。我从 python 对 yield 的帮助中了解到这一点,并且认为它们具有相似的概念。
  • 感谢 Marius、很好的解释和 JohnDRoach 我认为您的重点回答了我所问的问题 - “重新开始执行......”绝对是理解使用 yield 的关键。再次感谢大家。
【解决方案2】:

yield return 使编译器构建一个状态机,该状态机使用您的方法体实现IEnumerable&lt;T&gt;。它从您的方法中返回一个对象,而不会在您编写它时实际调用方法的主体 - 编译器已将其替换为更复杂的东西。

当您在状态机生成的IEnumerator&lt;T&gt; 上调用MoveNext() 时(例如在foreach 循环期间),状态机将执行您的方法代码,直到它到达第一个yield return 语句。然后它将Current 的值设置为您返回的任何值,然后将控制权交还给调用者。

实际上,看起来您的方法体每次迭代都会执行一次,并且每次到达yield return 语句时循环都会“中断”。

如果您在方法的 while 循环中放置断点,您将看到堆栈包含对编译器生成类型的 MoveNext() 的调用,您的方法主体已成为该类型的一部分。

【讨论】:

  • 感谢悲剧演员。这就是问题的关键:每次迭代只执行一次方法体的外观。很好的解释。
【解决方案3】:

在高层次上,您可以将yield 视为“返回一个值并冻结方法的当前状态”。下次调用生成器时,该方法将解冻并从 yield' 之后的行开始恢复。因此,任何仅在方法开始处而不是实际上在存在yield 的循环中的行只会被调用一次,它不会重新启动整个方法。

在低级别上,yield 是由编译器实现的,将您的方法转换为状态机,在该状态机中,在方法的开头添加了一个跳转表,以及我们采用哪个跳转(我们开始执行哪一行代码在调用该方法时)由生成器最后进入的“状态”确定。类似的编码技术用于等待/异步状态机,并允许在更容易理解的情况下对程序员隐藏很多复杂性型号。

【讨论】:

  • 感谢 Patashu,yield 的实现看起来相当复杂,正如你所说,隐藏了很多复杂性。
【解决方案4】:

yield 运算符将强制编译器创建一个自定义类来实现您的逻辑。 理解它的更好方法是反编译结果 exe 并观察它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-01-05
    • 1970-01-01
    • 2018-01-06
    • 2015-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多