C# 中的迭代器 – 深入探讨





4.00/5 (4投票s)
C# 中的迭代器
引言
本文深入分析了 C# 的 yield
关键字在底层是如何工作的。
如果您对 yield
关键字一无所知,或者从未用过它,请查看我在 我的原始博客上的 C# 迭代器文章 或 CodeProject 上的文章。
使用迭代器很简单,但了解它的底层工作原理总是有好处的,对吧?
好吧,为了便于理解,让我们举一个简单的 C# 方法示例,该方法返回一个值列表。
这是代码
public class InDepth
{
static IEnumerator DoSomething()
{
yield return "start";
for (int i = 1; i < 3; i++)
{
yield return i.ToString();
}
yield return "end";
}
}
很简单,不是吗?让我们看看编译后的代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace YieldDemo
{
public class InDepth
{
public InDepth()
{
base..ctor();
}
private static IEnumerator DoSomething()
{
InDepth.<DoSomething>d__0 doSomethingD0 = new InDepth.<DoSomething>d__0(0);
return (IEnumerator) doSomethingD0;
}
[CompilerGenerated]
private sealed class <DoSomething>d__0 : IEnumerator<object>, IEnumerator, IDisposable
{
private object <>2__current;
private int <>1__state;
public int <i>5__1;
object IEnumerator<object>.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
[DebuggerHidden]
public <DoSomething>d__0(int <>1__state)
{
base.\u002Ector();
this.<>1__state = param0;
}
bool IEnumerator.MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>2__current = (object) "start";
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
this.<i>5__1 = 1;
break;
case 2:
this.<>1__state = -1;
++this.<i>5__1;
break;
case 3:
this.<>1__state = -1;
goto default;
default:
return false;
}
if (this.<i>5__1 < 3)
{
this.<>2__current = (object) this.<i>5__1.ToString();
this.<>1__state = 2;
return true;
}
else
{
this.<>2__current = (object) "end";
this.<>1__state = 3;
return true;
}
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
}
}
}
震惊!我只写了不到 10 行代码,但编译器却生成了太多行。好吧,编译器会创建自动生成的有限状态机来实现 yield
功能。让我们检查一下已编译的代码。
总体观察
- 所示代码不是有效的 C# 代码:是的,代码无效。我们将使用有效的 C# 代码来编写程序和逻辑,如果编译器使用相同的有效代码,它会在编译过程中导致与方法和变量声明的冲突。
- 其中一些方法带有
[CompilerGenerated]
和[DebuggerHidden]
属性。编译器生成的属性将编译器生成的元素与用户生成的元素区分开来,而DebuggerHidden
属性会阻止方法进行调试。 <DoSomething>d__0
实现了三个接口:IEnumerator<object>
、IEnumerator
和IDisposable
,但我们只实现了一个接口。好吧,即使我们实现了非泛型形式的IEnumerator
,编译器也实现了一个泛型形式的IEnumerator
。IEnumerator<object>
暗示了另外两个接口。
<DoSomething>d__0
中发生了大量的魔法。让我们仔细看看。
- 方法中声明了三个变量。分别是
<>1__state
、<>2__current
和<i>5__1
。<>1_state
负责跟踪代码已执行到的位置。<>2__current
将返回迭代器的当前值。<i>5__1
只是计数变量。 State
和current
被声明为private
,而count
被声明为public
。如果我们在 Iterator 块中使用任何参数,那些变量也将是public
。- 这里有一个重要的注意事项。
DoSomething()
方法调用<DoSomething>d__0
,该方法始终向构造函数传递0
。此参数可能因 Iterator 块使用的返回类型而异。例如,如果我们使用IEnumerable<int>
作为返回类型,则它会传递初始值“-2
”,而不是0
。 Current
属性有两个版本。它们都返回<>2__current
。MoveNext()
、Reset
和Dispose
是已实现的方法。Reset()
方法始终抛出NotSupportedException
异常。这通常符合 C# 规范。- 您在 Iterator 块中编写的任何代码都会进入
MoveNext()
方法。它总是一个switch
语句。current
、state
和count
的值在此方法本身中被修改。您可以观察到switch
的条件语句是当前状态。根据当前状态,值被修改并返回。
迭代器不是独立运行的。当调用迭代器方法时,它只是被创建。实际过程从调用 MoveNext()
开始。MoveNext()
被反复调用,直到达到 yield break
、yield return
或方法末尾。
在迭代器中需要注意的一个重要事项是,您不能从具有关联 catch
块的 try
块中进行 yield
,也不能从具有 catch
和 finally
块的 try
块中进行 yield
。但是,您可以从仅具有 finally
块而没有 catch
的 try
块中进行 yield
。
到目前为止,我们一直从 Iterator 块返回 IEnumerator
。让我们将 IEnumerator
替换为 IEnumerable
。还要注意,之前从
Iterator 块返回的 IEnumerator
是一个非泛型版本。我们将再次使用具有泛型形式的 IEnumerable
来实现 Iterator 块。修改后的代码如下:
static IEnumerable<string> DoSomething(){
yield return "start";
for (int i = 1; i < 3; i++)
{
yield return i.ToString();
}
yield return "end";
}
另外,让我们把编译后的代码放在这里。我们将检查 IEnumerable
实现有什么新变化。这是代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace YieldDemo
{
public class InDepth
{
public InDepth()
{
base..ctor();
}
private static IEnumerable<string> DoSomething()
{
InDepth.<DoSomething>d__0 doSomethingD0 = new InDepth.<DoSomething>d__0(-2);
return (IEnumerable<string>) doSomethingD0;
}
[CompilerGenerated]
private sealed class <DoSomething>d__0 : IEnumerable<string>,
IEnumerable, IEnumerator<string>, IEnumerator, IDisposable
{
private string <>2__current;
private int <>1__state;
private int <>l__initialThreadId;
public int <i>5__1;
string IEnumerator<string>.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden] get
{
return (object) this.<>2__current;
}
}
[DebuggerHidden]
public <DoSomething>d__0(int <>1__state)
{
base..ctor();
this.<>1__state = param0;
this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
InDepth.<DoSomething>d__0 doSomethingD0;
if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
{
this.<>1__state = 0;
doSomethingD0 = this;
}
else
doSomethingD0 = new InDepth.<DoSomething>d__0(0);
return (IEnumerator<string>) doSomethingD0;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.String>.GetEnumerator();
}
bool IEnumerator.MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>2__current = "start";
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
this.<i>5__1 = 1;
break;
case 2:
this.<>1__state = -1;
++this.<i>5__1;
break;
case 3:
this.<>1__state = -1;
goto default;
default:
return false;
}
if (this.<i>5__1 < 3)
{
this.<>2__current = this.<i>5__1.ToString();
this.<>1__state = 2;
return true;
}
else
{
this.<>2__current = "end";
this.<>1__state = 3;
return true;
}
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
}
}
}
观察
- 首先,
DoSomething()
方法的返回类型已更改为IEnumerable<string>
。 - 同样,值得注意的是,传递给
<DoSomething>d__0()
构造函数的参数已从0
更改为-2
。 - 编译器生成的
<DoSomething>d__0
类除了其他接口外,还实现了IEnumerable<string>
、IEnumerable
、IEnumerator<string>
。 - 密封类中
IEnumerator<int>
的实现与IEnumerator
几乎相同。Current
属性只是返回当前值,Reset
抛出相同的异常,MoveNext()
具有相同的逻辑。 - 添加了一个
private
变量<>l__initialThreadId
,在构造函数中设置为当前线程。
那么,发生了什么?当创建 IEnumerable<string>
的实例时,会调用 GetEnumerator()
方法,该方法返回一个 IEnumerator
接口,并继续执行 IEnumerator
中的方法。此外,对集合的只读访问已启用。正是 MoveNext()
方法被一遍又一遍地操作以惰性地返回这些值。
为什么对 DoSomething
构造函数的初始调用从 0
更改为 -2
?好吧,这些是告诉编译器它们处于什么状态的代码。以下是状态机运行的状态:
0
:表示“工作尚未开始”(之前)。-1
:表示“工作正在进行中”(运行中)或“工作已完成”(之后)。-2
:这特定于IEnumerable
。这是调用GetEnumerator
之前的IEnumerable
的初始状态。- 大于
0
:表示恢复状态。
另外,这里需要注意的一点是,-2
状态是 IEnumerable
特有的。其他状态是 IEnumerator
特有的。因此,当 IEnumerable
调用 GetEnumerator
方法时,状态将更改为 0
,依此类推,因为它返回 IEnumerator
接口。
就是这样!乍一看,这看起来很奇怪,但当我们开始慢慢理解时,它比我们预期的要容易得多。
请分享您对此帖子的想法和评论!谢谢!