关于c#:Yield语句对程序流的影响

Yield statement's effect on program flow

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 +"
"
);
        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变量更新为最新的幂结果,即2、4、8、16、32。

由于不完全理解yield的用法,我希望在值通过ComputePower时多次调用ComputePower,并且我将看到"Arguments to ComputePower are"等。控制台写入发生5次。但实际发生的是,似乎只调用一次ComputePower方法。每次运行我只看到一次"Arguments to ComputePower.."字符串。

有人能解释为什么会这样吗?它与yield关键字有关吗?


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

来自MSDN:

You consume an iterator method by using a foreach statement or LINQ query. Each iteration of the foreach loop calls the iterator method. When a yield return statement is reached in the iterator method, expression is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.


yield return使编译器构建一个状态机,该状态机使用方法体实现IEnumerable。它从方法中返回一个对象,而不在编写过程中调用方法体——编译器用更复杂的内容替换了它。

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

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

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


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

在较低的层次上,yield是由编译器实现的,它将您的方法转换为一个状态机,在该方法的开头添加一个跳转表,我们所采用的跳转(当您调用该方法时,我们开始执行的代码的哪一行)是由生成器的最后一个"状态"决定的。一种类似的编码技术被用于等待/异步状态机,并允许在一个更容易理解的模型下对程序员隐藏大量的复杂性。


yield运算符将强制编译器创建一个将实现逻辑的自定义类。更好的理解方法是将结果exe反编译并监视到其中。