关于C#:yield语句实现

yield statement implementation

我想以一种易于理解的形式了解有关yield声明的所有信息。

我已经阅读过yield语句及其在实现迭代器模式时的易用性。然而,大部分是非常干燥的。我想了解一下微软是如何处理收益率的。

另外,你什么时候使用屈服点?


以下是从陈瑞蒙的博客开始:

  • C语言中迭代器的实现及其后果(第1部分)
  • C语言中迭代器的实现及其后果(第2部分)
  • C语言中迭代器的实现及其后果(第3部分)

yield通过在内部构建一个状态机来工作。当例程退出并下次从该状态恢复时,它存储该例程的当前状态。

您可以使用Reflector来查看编译器是如何实现它的。

当您想停止返回结果时,使用yield break。如果没有yield break语句,编译器会在函数末尾假定一个(就像正常函数中的return;语句一样)。


正如梅尔达所说,它建立了一个状态机。

除了使用Reflector(另一个很好的建议),您可能会发现我关于迭代器块实现的文章很有用。如果没有finally块,这将相对简单,但它们引入了一个额外的复杂度维度!


让我们倒回去一点:yield关键字的翻译和许多其他人说的对状态机的翻译一样。

实际上,这并不完全像使用将在后台使用的内置实现,而是编译器通过实现一个相关接口(包含yield关键字的方法的返回类型)将与yield相关的代码重写到状态机。

(有限)状态机只是一段代码,根据您在代码中的位置(取决于前一个状态,输入)转到另一个状态操作,当您使用和生成方法返回类型为IEnumeratorIEnumerator时,会发生这种情况。yield关键字将创建另一个操作,从上一个状态移到下一个状态,因此在MoveNext()实现中创建状态管理。

这正是C编译器/Roslyn要做的:检查是否存在yield关键字以及包含方法的返回类型,是否是IEnumeratorIEnumerableIEnumeratorIEnumerable,然后创建反映该方法的私有类,集成必要的变量和状态。

如果您对状态机以及编译器如何重写迭代的细节感兴趣,可以在GitHub上查看这些链接:

  • IteratorRewriter源代码
  • StateMachineRewriter:上述源代码的父类

小贴士1:AsyncRewriter(在编写async/await代码时使用)也继承自StateMachineRewriter,因为它还利用了后面的状态机。

如前所述,状态机在bool MoveNext()生成的实现中有很强的反映,其中有一个switch,有时是一些老式的goto,它基于一个状态字段,表示方法中不同状态的执行路径。

编译器从用户代码生成的代码看起来不太"好",主要是因为编译器在这里和那里添加了一些奇怪的前缀和后缀。

例如,代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class TestClass
{
    private int _iAmAHere = 0;

    public IEnumerator<int> DoSomething()
    {
        var start = 1;
        var stop = 42;
        var breakCondition = 34;
        var exceptionCondition = 41;
        var multiplier = 2;
        // Rest of the code... with some yield keywords somewhere below...

编译后,与上述代码段相关的变量和类型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestClass
{
    [CompilerGenerated]
    private sealed class <DoSomething>d__1 : IEnumerator<int>, IDisposable, IEnumerator
    {
        // Always present
        private int <>1__state;
        private int <>2__current;

        // Containing class
        public TestClass <>4__this;

        private int <start>5__1;
        private int <stop>5__2;
        private int <breakCondition>5__3;
        private int <exceptionCondition>5__4;
        private int <multiplier>5__5;

关于状态机本身,让我们来看一个非常简单的例子,其中有一个用于生成一些偶数/奇数的伪分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Example
{
    public IEnumerator<string> DoSomething()
    {
        const int start = 1;
        const int stop = 42;

        for (var index = start; index < stop; index++)
        {
            yield return index % 2 == 0 ?"even" :"odd";
        }
    }
}

将在MoveNext中翻译为:

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
35
36
37
38
private bool MoveNext()
{
    switch (<>1__state)
    {
        default:
            return false;
        case 0:
            <>1__state = -1;
            <start>5__1 = 1;
            <stop>5__2 = 42;
            <index>5__3 = <start>5__1;
            break;
        case 1:
            <>1__state = -1;
            goto IL_0094;
        case 2:
            {
                <>1__state = -1;
                goto IL_0094;
            }
            IL_0094:
            <index>5__3++;
            break;
    }
    if (<index>5__3 < <stop>5__2)
    {
        if (<index>5__3 % 2 == 0)
        {
            <>2__current ="even";
            <>1__state = 1;
            return true;
        }
        <>2__current ="odd";
        <>1__state = 2;
        return true;
    }
    return false;
}

正如您所看到的,这个实现远不是简单的,但它确实完成了任务!

小技巧2:IEnumerable/IEnumerable方法返回类型发生了什么?那么,它将生成一个实现IEnumerator的类,而不是生成一个实现IEnumerableIEnumerator的类,这样IEnumerator GetEnumerator()的实现将利用相同生成的类。

关于使用yield关键字时自动实现的几个接口的温馨提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface IEnumerable<out T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

public interface IEnumerator
{
    bool MoveNext();

    object Current { get; }

    void Reset();
}

您还可以使用不同的路径/分支和编译器重写的完整实现来检查这个示例。

这是使用sharplab创建的,您可以使用该工具尝试不同的yield相关的执行路径,并查看编译器将如何在MoveNext实现中将它们重写为状态机。

关于问题的第二部分,即yield break,这里已经回答了。

It specifies that an iterator has come to an end. You can think of
yield break as a return statement which does not return a value.