我想以一种易于理解的形式了解有关yield声明的所有信息。
我已经阅读过yield语句及其在实现迭代器模式时的易用性。然而,大部分是非常干燥的。我想了解一下微软是如何处理收益率的。
另外,你什么时候使用屈服点?
你不能同时得到"一切"和"容易理解",对不起!仅供参考,我将于7月2日从语言设计和实现的角度发表一系列关于这个特性的博客文章。
以下是从陈瑞蒙的博客开始:
C语言中迭代器的实现及其后果(第1部分)
C语言中迭代器的实现及其后果(第2部分)
C语言中迭代器的实现及其后果(第3部分)
yield通过在内部构建一个状态机来工作。当例程退出并下次从该状态恢复时,它存储该例程的当前状态。
您可以使用Reflector来查看编译器是如何实现它的。
当您想停止返回结果时,使用yield break。如果没有yield break语句,编译器会在函数末尾假定一个(就像正常函数中的return;语句一样)。
它是什么意思"程序的当前状态":处理器寄存器值,帧指针等?
看看连体衣
@tcraft-microsoft的规范实现不使用不同的堆栈/分段的堆栈等。它们使用堆分配的对象来存储状态。
正如梅尔达所说,它建立了一个状态机。
除了使用Reflector(另一个很好的建议),您可能会发现我关于迭代器块实现的文章很有用。如果没有finally块,这将相对简单,但它们引入了一个额外的复杂度维度!
让我们倒回去一点:yield关键字的翻译和许多其他人说的对状态机的翻译一样。
实际上,这并不完全像使用将在后台使用的内置实现,而是编译器通过实现一个相关接口(包含yield关键字的方法的返回类型)将与yield相关的代码重写到状态机。
(有限)状态机只是一段代码,根据您在代码中的位置(取决于前一个状态,输入)转到另一个状态操作,当您使用和生成方法返回类型为IEnumerator/IEnumerator时,会发生这种情况。yield关键字将创建另一个操作,从上一个状态移到下一个状态,因此在MoveNext()实现中创建状态管理。
这正是C编译器/Roslyn要做的:检查是否存在yield关键字以及包含方法的返回类型,是否是IEnumerator、IEnumerable、IEnumerator或IEnumerable,然后创建反映该方法的私有类,集成必要的变量和状态。
如果您对状态机以及编译器如何重写迭代的细节感兴趣,可以在GitHub上查看这些链接:
IteratorRewriter源代码
StateMachineRewriter:上述源代码的父类
小贴士1:AsyncRewriter(在编写async/await代码时使用)也继承自StateMachineRewriter,因为它还利用了后面的状态机。
如前所述,状态机在bool MoveNext()生成的实现中有很强的反映,其中有一个switch,有时是一些老式的goto,它基于一个状态字段,表示方法中不同状态的执行路径。
编译器从用户代码生成的代码看起来不太"好",主要是因为编译器在这里和那里添加了一些奇怪的前缀和后缀。
例如,代码:
public class TestClass
{
private int _iAmAHere = 0;
public IEnumerator 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...
编译后,与上述代码段相关的变量和类型如下:
public class TestClass
{
[CompilerGenerated]
private sealed class d__1 : IEnumerator, IDisposable, IEnumerator
{
// Always present
private int <>1__state;
private int <>2__current;
// Containing class
public TestClass <>4__this;
private int 5__1;
private int 5__2;
private int 5__3;
private int 5__4;
private int 5__5;
关于状态机本身,让我们来看一个非常简单的例子,其中有一个用于生成一些偶数/奇数的伪分支。
public class Example
{
public IEnumerator DoSomething()
{
const int start = 1;
const int stop = 42;
for (var index = start; index < stop; index++)
{
yield return index % 2 == 0 ?"even" :"odd";
}
}
}
将在MoveNext中翻译为:
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
5__1 = 1;
5__2 = 42;
5__3 = 5__1;
break;
case 1:
<>1__state = -1;
goto IL_0094;
case 2:
{
<>1__state = -1;
goto IL_0094;
}
IL_0094:
5__3++;
break;
}
if (5__3 < 5__2)
{
if (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的类,而不是生成一个实现IEnumerable和IEnumerator的类,这样IEnumeratorGetEnumerator()的实现将利用相同生成的类。
关于使用yield关键字时自动实现的几个接口的温馨提示:
public interface IEnumerable : IEnumerable
{
new IEnumerator GetEnumerator();
}
public interface IEnumerator : 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.