Yield语句对程序流的影响

本文关键字:影响 程序 语句 Yield | 更新日期: 2023-09-27 18:16:02

我试图理解在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语句对程序流的影响

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

从msdn:

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

yield return导致编译器构建一个状态机,该状态机使用你的方法体实现IEnumerable<T>。它从你的方法返回一个对象,而没有实际调用你编写的方法体——编译器已经用更复杂的东西取代了它。

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

在实践中,看起来好像你的方法体每次迭代执行一次,并且每次到达yield return语句时循环被"中断"。

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

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

在较低的层次上,yield是由编译器实现的,将你的方法转换成一个状态机,在方法开始时添加一个跳转表,我们采取的跳转(当你调用方法时,我们开始执行的代码行)是由生成器最后进入的"状态"决定的。类似的编码技术用于await/async状态机,并允许在更容易理解的模型下对程序员隐藏许多复杂性。

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