本文共 13435 字,大约阅读时间需要 44 分钟。
在我之前的一篇博客《》的最后,我以总结和后记的方式涉及到一部分迭代器的知识。但是觉得还是不够过瘾,很多需要说清楚的内容还是含糊不清,所以本文就专门写一下c#中的迭代器吧。
\\\\首先思考一下,在什么情景下我们需要使用到迭代器?
\\假设我们有一个数据容器(可能是Array,List,Tree等等),对我们这些使用者来说,我们显然希望这个数据容器能提供一种无需了解它的内部实现就可以获取其元素的方法,无论它是Array还是List或者别的什么,我们希望可以通过相同的方法达到我们的目的。
\\此时,迭代器模式(iterator pattern)便应运而生,它通过持有迭代状态,追踪当前元素并且识别下一个需要被迭代的元素,从而可以让使用者透过特定的界面巡访容器中的每一个元素而不用了解底层的实现。
\\那么,在c#中,迭代器到底是以一个怎样的面目出现的呢?
\\如我们所知,它们被封装在IEnumerable和IEnumerator这两个接口中(当然,还有它们的泛型形式,要注意的是泛型形式显然是强类型的。且IEnumerator\u0026lt;T\u0026gt;
实现了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 IEnumerable\u0026lt;out T\u0026gt; : IEnumerable\{\ IEnumerator\u0026lt;T\u0026gt; GetEnumerator();\ IEnumerator GetEnumerator(); \}\\\
IEnumerator泛型形式:
\\//IEnumerator泛型形式\public interface IEnumerator\u0026lt;out T\u0026gt; : 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循环。
\\\\到此,各位看官是否和曾经的我有相同的疑惑呢?那就是为何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 IEnumerable\u0026lt;string\u0026gt; GetEnumerableTest()\ {\ yield return \"begin\";\\ for (int i=0; i \u0026lt; 10; i++)\ {\ yield return i.ToString();\ }\\ yield return \"end\";\ }\}\\\
输出结果如图:
\\ \\OK,那么我就给各位捋一下这段代码的执行过程。
\\这个例子中迭代器的执行过程,我已经给各位看官简单的描述了一下。但是还有几点需要关注的,我也想提醒各位注意一下。
\\好了,简单总结了一下C#中的迭代器的外观。那么接下来,我们继续向内部前进,来看看迭代器究竟是如何实现的。
\\\\上一节我们已经从外部看到了IEnumerable和IEnumerator这两个接口的用法了,但是它们的内部到底是如何实现的呢?两者之间又有何区别呢?
\\既然要深入迭代器的内部,这就是一个不得不面对的问题。
\\那么我就写一个小程序,之后再通过反编译的方式,看看在我们自己手动写的代码背后,编译器究竟又给我们做了哪些工作吧。
\\为了简便起见,这个小程序仅仅实现一个按顺序返回0-9这10个数字的功能。
\\\\首先,我们定义一个返回IEnumerator的方法TestIterator()。
\\//IEnumerator\u0026lt;T\u0026gt;测试\using System;\using System.Collections;\\class Test\{\ static IEnumerator\u0026lt;int\u0026gt; TestIterator()\ {\ for (int i = 0; i \u0026lt; 10; i++)\ {\ yield return i;\ }\ }\}\\\
接下来,我们看看反编译之后的代码,探查一下编译器到底为我们做了什么吧。
\\internal class Test\{\ // Methods 注,此时还没有执行任何我们写的代码\ private static IEnumerator\u0026lt;int\u0026gt; TestIterator()\ {\ return new \u0026lt;TestIterator\u0026gt;d__0(0);\ }\\ // Nested Types 编译器生成的类,用来实现迭代器。\ [CompilerGenerated]\ private sealed class \u0026lt;TestIterator\u0026gt;d__0 : IEnumerator\u0026lt;int\u0026gt;, IEnumerator, IDisposable\ {\ // Fields 字段:state和current是默认出现的\ private int \u0026lt;\u0026gt;1__state;\ private int \u0026lt;\u0026gt;2__current;\ public int \u0026lt;i\u0026gt;5__1;//\u0026lt;i\u0026gt;5__1来自我们迭代器块中的局部变量\\ // Methods 构造函数,初始化状态\ [DebuggerHidden]\ public \u0026lt;TestIterator\u0026gt;d__0(int \u0026lt;\u0026gt;1__state)\ {\ this.\u0026lt;\u0026gt;1__state = \u0026lt;\u0026gt;1__state;\ }\ // 几乎所有的逻辑在这里\ private bool MoveNext()\ {\ switch (this.\u0026lt;\u0026gt;1__state)\ {\ case 0:\ this.\u0026lt;\u0026gt;1__state = -1;\ this.\u0026lt;i\u0026gt;5__1 = 0;\ while (this.\u0026lt;i\u0026gt;5__1 \u0026lt; 10)\ {\ this.\u0026lt;\u0026gt;2__current = this.\u0026lt;i\u0026gt;5__1;\ this.\u0026lt;\u0026gt;1__state = 1;\ return true;\ Label_0046:\ this.\u0026lt;\u0026gt;1__state = -1;\ this.\u0026lt;i\u0026gt;5__1++;\ }\ break;\\ case 1:\ goto Label_0046;\ }\ return false;\ }\\ [DebuggerHidden]\ void IEnumerator.Reset()\ {\ throw new NotSupportedException();\ }\\ void IDisposable.Dispose()\ {\ }\\ // Properties\ int IEnumerator\u0026lt;int\u0026gt;.Current\ {\ [DebuggerHidden]\ get\ {\ return this.\u0026lt;\u0026gt;2__current;\ }\ }\\ object IEnumerator.Current\ {\ [DebuggerHidden]\ get\ {\ return this.\u0026lt;\u0026gt;2__current;\ }\ }\ }\}\\\
我们先全面的看一下反编译之后的代码,可以发现几乎所有的逻辑都发生在MoveNext()方法中。那么之后我们再详细介绍下它,现在我们先从上到下把代码捋一遍。
\\\u0026lt;TestIterator\u0026gt;d__0
(编译器生成的用来实现迭代器的类)的构造函数。而这个构造函数会设置迭代器的初始状态,此时的参数为0,而构造函数会将0赋值给记录迭代器状态的字段:this.\u0026lt;\u0026gt;1__state = \u0026lt;\u0026gt;1__state
;。注意,此时我们自己的代码并没有执行。\\u0026lt;TestIterator\u0026gt;d__0
这个类实现了3个接口:IEnumerator\u0026lt;int\u0026gt;
, IEnumerator, IDisposable。\\u0026lt;TestIterator\u0026gt;d__0
类有3个字段:\u0026lt;\u0026gt;1__state
,\u0026lt;\u0026gt;2__current
, \u0026lt;i\u0026gt;5__1
。其中,\u0026lt;\u0026gt;1__state
私有字段标识迭代器的状态,\u0026lt;\u0026gt;2__current
私有字段则追踪当前的值,而\u0026lt;i\u0026gt;5__1
共有字段则是我们在迭代器块中定义的局部变量i。\IEnumerator\u0026lt;int\u0026gt;
的实现中,Current都是单纯的返回\u0026lt;\u0026gt;2__current
的值。\OK,IEnumerator接口我们看完了。下面再来看看另一个接口IEnumerable吧。
\\\\依样画葫芦,这次我们仍然是写一个实现按顺序返回0-9这10个数字的功能的小程序,只不过返回类型变为IEnumerable\u0026lt;T\u0026gt;
。
using System;\using System.Collections.Generic;\\class Test\{\ static IEnumerable\u0026lt;int\u0026gt; TestIterator()\ {\ for (int i = 0; i \u0026lt; 10; i++)\ {\ yield return i;\ }\ }\}\\\
之后,我们同样通过反编译,看看编译器又背着我们做了什么。
\\internal class Test\{\ private static IEnumerable\u0026lt;int\u0026gt; TestIterator()\ {\ return new \u0026lt;TestIterator\u0026gt;d__0(-2);\ }\\ private sealed class \u0026lt;TestIterator\u0026gt;d__0 : IEnumerable\u0026lt;int\u0026gt;, IEnumerable, IEnumerator\u0026lt;int\u0026gt;, IEnumerator, IDisposable\ {\ // Fields\ private int \u0026lt;\u0026gt;1__state;\ private int \u0026lt;\u0026gt;2__current;\ private int \u0026lt;\u0026gt;l__initialThreadId;\ public int \u0026lt;count\u0026gt;5__1;\\ public \u0026lt;TestIterator\u0026gt;d__0(int \u0026lt;\u0026gt;1__state)\ {\ this.\u0026lt;\u0026gt;1__state = \u0026lt;\u0026gt;1__state;\ this.\u0026lt;\u0026gt;l__initialThreadId = Thread.CurrentThread.ManagedThreadId;\ }\\ private bool MoveNext()\ {\ switch (this.\u0026lt;\u0026gt;1__state)\ {\ case 0:\ this.\u0026lt;\u0026gt;1__state = -1;\ this.\u0026lt;count\u0026gt;5__1 = 0;\ while (this.\u0026lt;count\u0026gt;5__1 \u0026lt; 10)\ {\ this.\u0026lt;\u0026gt;2__current = this.\u0026lt;count\u0026gt;5__1;\ this.\u0026lt;\u0026gt;1__state = 1;\ return true;\ Label_0046:\ this.\u0026lt;\u0026gt;1__state = -1;\ this.\u0026lt;count\u0026gt;5__1++;\ }\ break;\\ case 1:\ goto Label_0046;\ }\ return false;\ }\\ IEnumerator\u0026lt;int\u0026gt; IEnumerable\u0026lt;int\u0026gt;.GetEnumerator()\ {\ if ((Thread.CurrentThread.ManagedThreadId == this.\u0026lt;\u0026gt;l__initialThreadId) \u0026amp;\u0026amp; (this.\u0026lt;\u0026gt;1__state == -2))\ {\ this.\u0026lt;\u0026gt;1__state = 0;\ return this;\ }\ return new Test.\u0026lt;TestIterator\u0026gt;d__0(0);\ }\\ IEnumerator IEnumerable.GetEnumerator()\ {\ return ((IEnumerable\u0026lt;Int32\u0026gt;) this).GetEnumerator();\ }\\ void IEnumerator.Reset()\ {\ throw new NotSupportedException();\ }\\ void IDisposable.Dispose()\ {\ }\\ int IEnumerator\u0026lt;int\u0026gt;.Current\ {\ get\ {\ return this.\u0026lt;\u0026gt;2__current;\ }\ }\\ object IEnumerator.Current\ {\ get\ {\ return this.\u0026lt;\u0026gt;2__current;\ }\ }\ }\}\\\
看到反编译出的代码,我们就很容易能对比出区别。
\\\u0026lt;TestIterator\u0026gt;d__0
类不仅实现了IEnumerable\u0026lt;int\u0026gt;
接口,而且还实现了IEnumerator\u0026lt;int\u0026gt;
接口。\IEnumerator\u0026lt;int\u0026gt;
的实现都和上面一样。IEnumerator的Reset方法会抛出NotSupportedException异常,而IEnumerator和IEnumerator\u0026lt;int\u0026gt;
的Current仍旧会返回\u0026lt;\u0026gt;2__current
字段的值。\\u0026lt;TestIterator\u0026gt;d__0
类的构造函数时,传入的参数由上面的0变成了-2:new \u0026lt;TestIterator\u0026gt;d__0(-2);
。也就是说此时的初始状态是-2。\\u0026lt;\u0026gt;l__initialThreadId
,且会在\u0026lt;TestIterator\u0026gt;d__0
的构造函数中被赋值,用来标识创建该实例的线程。\this.\u0026lt;\u0026gt;1__state = 0;return this;
要么就返回一个新的\u0026lt;TestIterator\u0026gt;d__0
实例,且初始状态置为0:return new Test.\u0026lt;TestIterator\u0026gt;d__0(0);
\所以,从这些对比中我们能发现些什么吗?思考一下我们经常使用的一些用法,包括我在上一节中提供的小例子。不错,我们会创建一个IEnumerable\u0026lt;T\u0026gt;
的实例,之后一些语句(例如foreach)会去调用GetEnumerator方法获取一个Enumerator\u0026lt;T\u0026gt;
的实例,之后迭代数据,最终结束后释放掉迭代器的实例(这一步foreach会帮我们做)。
而分析IEnumerable的GetEnumerator方法:
\\IEnumerator\u0026lt;int\u0026gt; IEnumerable\u0026lt;int\u0026gt;.GetEnumerator()\{\ if ((Thread.CurrentThread.ManagedThreadId == this.\u0026lt;\u0026gt;l__initialThreadId) \u0026amp;\u0026amp; (this.\u0026lt;\u0026gt;1__state == -2))\ {\ this.\u0026lt;\u0026gt;1__state = 0;\ return this;\ }\ return new Test.\u0026lt;TestIterator\u0026gt;d__0(0);\}\\\
我们可以发现,-2这个状态,也就是此时的初始状态,表明了GetEnumerator()方法还没有执行。而0这个状态,则表明已经准备好了迭代,但是MoveNext()尚未调用过。
\\当在不同的线程上调用GetEnumerator方法或者是状态不是-2(证明已经不是初始状态了),则GetEnumerator方法会返回一个\u0026lt;TestIterator\u0026gt;d__0
类的新实例用来保存不同的状态。
OK,我们深入了迭代器的内部,发现了原来它的实现主要依靠的是一个状态机。那么,下面就让我继续和大伙聊聊这个状态机是如何管理状态的。
\\\\根据Ecma-334标准,也就是c#语言标准的小节,我们可以知道迭代器有4种可能状态:
\\而其中before状态是作为初始状态出现的。
\\在我们讨论状态如何切换之前,我还要带领大家回想一下上面提到的,也就是在调用一个使用了迭代器块,返回类型为一个IEnumerator或IEnumerable接口的方法时,这个方法并非立刻执行我们自己写的代码的。而是会创建一个编译器生成的类的实例,之后当调用MoveNext()方法时(当然如果方法的返回类型是IEnumerable,则要先调用GetEnumerator()方法),我们的代码才会开始执行,直到遇到第一个yield return语句或yield break语句,此时会返回一个布尔值来判断迭代是否结束。当下次再调用MoveNext()方法时,我们的方法会继续从上一个yield return语句处开始执行。
\\为了能够直观的观察状态的切换,下面我提供另一个例子:
\\class Test\{\\ static IEnumerable\u0026lt;int\u0026gt; TestStateChange()\ {\ Console.WriteLine(\"----我TestStateChange是第一行代码\");\ Console.WriteLine(\"----我是第一个yield return前的代码\");\ yield return 1;\ Console.WriteLine(\"----我是第一个yield return后的代码\");\\ Console.WriteLine(\"----我是第二个yield return前的代码\");\ yield return 2;\ Console.WriteLine(\"----我是第二个yield return前的代码\");\ }\\ static void Main()\ {\ Console.WriteLine(\"调用TestStateChange\");\ IEnumerable\u0026lt;int\u0026gt; iteratorable = TestStateChange();\ Console.WriteLine(\"调用GetEnumerator\");\ IEnumerator\u0026lt;int\u0026gt; iterator = iteratorable.GetEnumerator();\ Console.WriteLine(\"调用MoveNext()\");\ bool hasNext = iterator.MoveNext();\ Console.WriteLine(\"是否有数据={0}; Current={1}\
转载地址:http://cabsl.baihongyu.com/