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