让我们稍微修改一下您的原始代码块,将其归结为基本要素,同时仍保持其足够有趣以供分析。这并不完全等同于您发布的内容,但我们仍在使用迭代器的值。
class Disposable : IDisposable {
public void Dispose() {
Console.WriteLine("Disposed!");
}
}
IEnumerable<int> CreateEnumerable() {
int i = 0;
using (var d = new Disposable()) {
while (true) yield return ++i;
}
}
void UseEnumerable() {
foreach (int i in CreateEnumerable()) {
Console.WriteLine(i);
if (i == 10) break;
}
}
这将在打印Disposed!之前打印从1到10的数字
幕后究竟发生了什么?还有很多。让我们先处理外层,UseEnumerable。 foreach 是以下语法糖:
var e = CreateEnumerable().GetEnumerator();
try {
while (e.MoveNext()) {
int i = e.Current;
Console.WriteLine(i);
if (i == 10) break;
}
} finally {
e.Dispose();
}
对于确切的细节(因为即使这是简化了一点)我推荐你the C# language specification,第 8.8.4 节。这里的重要一点是foreach 需要隐式调用枚举器的Dispose。
接下来,CreateEnumerable 中的 using 语句也是语法糖。事实上,让我们把整个事情写成原始语句,这样我们以后可以更清楚地理解翻译:
IEnumerable<int> CreateEnumerable() {
int i = 0;
Disposable d = new Disposable();
try {
repeat:
i = i + 1;
yield return i;
goto repeat;
} finally {
d.Dispose();
}
}
实现迭代器块的确切规则在语言规范的第 10.14 节中有详细说明。它们是根据抽象操作而不是代码给出的。 C# in Depth 中给出了关于 C# 编译器生成什么样的代码以及每个部分的作用的很好的讨论,但我将给出一个仍然符合规范的简单翻译。重申一下,这不是编译器实际上会产生的,但它是一个足够好的近似值来说明正在发生的事情,并省略了处理线程和优化的更多毛茸茸的部分。
class CreateEnumerable_Enumerator : IEnumerator<int> {
// local variables are promoted to instance fields
private int i;
private Disposable d;
// implementation of Current
private int current;
public int Current => current;
object IEnumerator.Current => current;
// State machine
enum State { Before, Running, Suspended, After };
private State state = State.Before;
// Section 10.14.4.1
public bool MoveNext() {
switch (state) {
case State.Before: {
state = State.Running;
// begin iterator block
i = 0;
d = new Disposable();
i = i + 1;
// yield return occurs here
current = i;
state = State.Suspended;
return true;
}
case State.Running: return false; // can't happen
case State.Suspended: {
state = State.Running;
// goto repeat
i = i + 1;
// yield return occurs here
current = i;
state = State.Suspended;
return true;
}
case State.After: return false;
default: return false; // can't happen
}
}
// Section 10.14.4.3
public void Dispose() {
switch (state) {
case State.Before: state = State.After; break;
case State.Running: break; // unspecified
case State.Suspended: {
state = State.Running;
// finally occurs here
d.Dispose();
state = State.After;
}
break;
case State.After: return;
default: return; // can't happen
}
}
public void Reset() { throw new NotImplementedException(); }
}
class CreateEnumerable_Enumerable : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
return new CreateEnumerable_Enumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
IEnumerable<int> CreateEnumerable() {
return new CreateEnumerable_Enumerable();
}
这里的关键是代码块在出现yield return 或yield break 语句时被拆分,迭代器负责在中断时记住“我们在哪里”。正文中的任何finally 块都会延迟到Dispose。代码中的无限循环实际上不再是无限循环,因为它被周期性的yield return 语句中断。请注意,因为finally 块实际上不再是finally 块,当您处理迭代器时,它的执行不太确定。这就是为什么使用foreach(或确保在finally 块中调用迭代器的Dispose 方法的任何其他方式)是必不可少的原因。
这是一个简化的例子;当您使循环更复杂、引入异常等时,事情会变得更加有趣。 “只是让这项工作”的负担由编译器承担。