匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密
在匹夫的上一篇文章《匹夫细说C#不是“栈类型”的值类型从生命周期聊存储位置》的最后匹夫以总结和后记的方式涉及到一部分迭代器的知识。但是觉得还是不够过瘾很多需要说清楚的内容还是含糊不清所以这周就专门写一下c#中的迭代器吧。0x01 你好迭代器首先思考一下在什么情景下我们需要使用到迭代器假设我们有一个数据容器可能是ArrayListTree等等对我们这些使用者来说我们显然希望这个数据容器能提供一种无需了解它的内部实现就可以获取其元素的方法无论它是Array还是List或者别的什么我们希望可以通过相同的方法达到我们的目的。此时迭代器模式iterator pattern便应运而生它通过持有迭代状态追踪当前元素并且识别下一个需要被迭代的元素从而可以让使用者透过特定的界面巡访容器中的每一个元素而不用了解底层的实现。那么在c#中迭代器到底是以一个怎样的面目出现的呢如我们所知它们被封装在IEnumerable和IEnumerator这两个接口中当然还有它们的泛型形式要注意的是泛型形式显然是强类型的。且IEnumeratorT实现了IDisposable接口。IEnumerable非泛型形式://IEnumerable非泛型形式 [ComVisibleAttribute(True)] [GuidAttribute(496B0ABE-CDEE-11d3-88E8-00902754C43A)] public interface IEnumerable { IEnumerator GetEnumerator(); }IEnumerator非泛型形式://IEnumerator非泛型形式 [ComVisibleAttribute(true)] [GuidAttribute(496B0ABF-CDEE-11d3-88E8-00902754C43A)] public interface IEnumerator { Object Current {get;} bool MoveNext(); void Reset(); }IEnumerable泛型形式://IEnumerable泛型形式 public interface IEnumerableout T : IEnumerable { IEnumeratorT GetEnumerator(); IEnumerator GetEnumerator(); }IEnumerator泛型形式://IEnumerator泛型形式 public interface IEnumeratorout T : IDisposable, IEnumerator { void Dispose(); Object Current {get;} T Current {get;} bool MoveNext(); void Reset(); } [ComVisibleAttribute(true)] public interface IDisposable { void Dispose(); }IEnumerable接口定义了一个可以获取IEnumerator的方法——GetEnumerator()。而IEnumerator则在目标序列上实现循环迭代使用MoveNext()方法以及Current属性来实现直到你不再需要任何数据或者没有数据可以被返回。使用这个接口可以保证我们能够实现常见的foreach循环。为什么会有2个接口到此各位看官是否和曾经的匹夫有相同的疑惑呢那就是为何IEnumerable自己不直接实现MoveNext()方法、提供Current属性呢为何还需要额外的一个接口IEnumerator来专门做这个工作OK假设有两个不同的迭代器要对同一个序列进行迭代。当然这种情况很常见比如我们使用两个嵌套的foreach语句。我们自然希望两者相安无事不要互相影响彼此。所以自然而然的我们需要保证这两个独立的迭代状态能够被正确的保存、处理。这也正是IEnumerator要做的工作。而为了不违背单一职责原则不使IEnumerable拥有过多职责从而陷入分工不明的窘境所以IEnumerable自己并没有实现MoveNext()方法。迭代器的执行步骤为了更直观的了解一个迭代器匹夫这里提供一个小例子。using System; using System.Collections.Generic; class Class1 { static void Main() { foreach (string s in GetEnumerableTest()) { Console.WriteLine(s); } } static IEnumerablestring GetEnumerableTest() { yield return begin; for (int i0; i 10; i) { yield return i.ToString(); } yield return end; } }输出结果如图OK那么匹夫就给各位捋一下这段代码的执行过程。Main调用GetEnumerableTest()方法GetEnumerableTest()方法会为我们创建一个编译器生成的新的类Class1/GetEnumerableTestc__Iterator0本例中的实例。注意此时GetEnumerableTest()方法中我们自己的代码尚未执行Main调用MoveNext()方法迭代器开始执行直到它遇到第一个yield return语句。此时迭代器会获取当前的值是“start”并且返回true以告知此时还有数据Main使用Current属性以获取数据并打印出来Main再次调用MoveNext()方法迭代器继续从上次遇到yield return的地方开始执行并且和之前一样直到遇到下一个yield return迭代器按照这种方式循环直到MoveNext()方法返回false以告知此时已经没有数据了这个例子中迭代器的执行过程匹夫已经给各位看官简单的描述了一下。但是还有几点需要关注的匹夫也想提醒各位注意一下。在第一次调用MoveNext()方法之前我们自己在GetEnumerableTest中的代码不会执行之后调用MoveNext()方法时会从上次暂停yield return的地方开始。编译器会保证GetEnumerableTest方法中的局部变量能够被保留换句话说虽然本例中的i是值类型实例但是它的值其实是被迭代器保存在堆上的这样才能保证每次调用MoveNext时它是可用的。这也是匹夫上一篇文章中说迭代器块中的局部变量会被分配在堆上的原因。好啦简单总结了一下C#中的迭代器的外观。那么接下来我们继续向内部前进来看看迭代器究竟是如何实现的。0x02 原来是状态机呀上一节我们已经从外部看到了IEnumerable和IEnumerator这两个接口的用法了但是它们的内部到底是如何实现的呢两者之间又有何区别呢既然要深入迭代器的内部这就是一个不得不面对的问题。那么匹夫就写一个小程序之后再通过反编译的方式看看在我们自己手动写的代码背后编译器究竟又给我们做了哪些工作吧。为了简便起见这个小程序仅仅实现一个按顺序返回0-9这10个数字的功能。IEnumerator的内部实现首先我们定义一个返回IEnumeratorT的方法TestIterator()。//IEnumeratorT测试 using System; using System.Collections; class Test { static IEnumeratorint TestIterator() { for (int i 0; i 10; i) { yield return i; } } }接下来我们看看反编译之后的代码探查一下编译器到底为我们做了什么吧。internal class Test { // Methods 注此时还没有执行任何我们写的代码 private static IEnumeratorint TestIterator() { return new TestIteratord__0(0); } // Nested Types 编译器生成的类用来实现迭代器。 [CompilerGenerated] private sealed class TestIteratord__0 : IEnumeratorint, IEnumerator, IDisposable { // Fields 字段state和current是默认出现的 private int 1__state; private int 2__current; public int i5__1;//i5__1来自我们迭代器块中的局部变量匹夫上一篇文章中提到过 // Methods 构造函数初始化状态 [DebuggerHidden] public TestIteratord__0(int 1__state) { this.1__state 1__state; } // 几乎所有的逻辑在这里 private bool MoveNext() { switch (this.1__state) { case 0: this.1__state -1; this.i5__1 0; while (this.i5__1 10) { this.2__current this.i5__1; this.1__state 1; return true; Label_0046: this.1__state -1; this.i5__1; } break; case 1: goto Label_0046; } return false; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } void IDisposable.Dispose() { } // Properties int IEnumeratorint.Current { [DebuggerHidden] get { return this.2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return this.2__current; } } } }