C#迭代器
C#迭代器
在.NET中,迭代器模式是通过IEnumerator和IEnumerable接口以及它们的泛型版本来实现的。如果某个类实现了IEnumerable接口,就说明它可以被迭代访问,调用GetEnumerator()方法将返回IEnumerator的实现,这个就是迭代器本身。
在C# 1.0中,利用foreach语句实现了访问迭代器的内置支持,让集合的遍历变得简单、明了。其实,foreach的实现就是调用GetEnumerator和MoveNext方法以及Current属性。所以说,在C# 1.0中要获得迭代器就必须实现IEnumerable接口中的GetEnumerator方法,要实现一个迭代器就要实现IEnumerator接口中的MoveNext和Reset方法
在C# 2.0中提供的语法糖来简化迭代器的实现,可以通过yield关键字来简化迭代器的实现。
C#多线程编程实例 线程与窗体交互【附源码】
C#数学运算表达式解释器
在C语言中解析JSON配置文件
C++ Primer Plus 第6版 中文版 清晰有书签PDF+源代码
C# 1.0中的迭代器实现
假设我们要实现一个字符列表类型,并且可以通过foreach来遍历这个类型。那么,在C# 1.0中,就要实现IEnumerable和IEnumerator接口。
namespace IteratorTest { class Program { static void Main(string[] args) { CharList charList = new CharList("Hello World"); foreach (var c in charList) { Console.WriteLine(c); } Console.Read(); } } class CharList : IEnumerable { public string TargetStr { get; set; } public CharList(string str) { this.TargetStr = str; } public IEnumerator GetEnumerator() { return new CharIterator(this.TargetStr); } } class CharIterator : IEnumerator { //引用要遍历的字符串 public string TargetStr { get; set; } //指出当前遍历的位置 public int position { get; set; } public CharIterator(string targetStr) { this.TargetStr = targetStr; this.position = this.TargetStr.Length; } public object Current { get { if (this.position == -1 || this.position == this.TargetStr.Length) { throw new InvalidOperationException(); } return this.TargetStr[this.position]; } } public bool MoveNext() { //如果满足继续遍历的条件,设置position的值 if (this.position != -1) { this.position--; } return this.position > -1; } public void Reset() { this.position = this.TargetStr.Length; } } }
在上面的例子中,CharIterator就是迭代器的实现,position字段存储当前的迭代位置,通过Current属性可以得到当前迭代位置的元素,MoveNext方法用于更新迭代位置,并且查看下一个迭代位置是不是有效的。
当我们通过VS单步调试下面语句的时候
foreach (var c in charList)
代码首先执行到foreach语句的charList处获得迭代器CharIterator的实例,然后代码执行到in会调用迭代器的MoveNext方法,最后变量c会得到迭代器Current属性的值;前面的步骤结束后,会开始一轮新的循环,调用MoveNext方法,获取Current属性的值。
C# 2.0通过yield简化迭代器实现
通过C# 1.0中迭代器的代码看到,要实现一个迭代器就要实现IEnumerator接口,然后实现IEnumerator接口中的MoveNext、Reset方法和Current属性。
在C# 2.0中可以直接使用yield语句来简化迭代器的实现。
class CharList : IEnumerable { public string TargetStr { get; set; } public CharList(string str) { this.TargetStr = str; } public IEnumerator GetEnumerator() { for (int index = this.TargetStr.Length; index > 0; index--) { yield return this.TargetStr[index-1]; } } }
通过上面的代码可以看到,通过使用yield return语句,我们可以替换掉整个CharIterator类。
yield return语句就是告诉编译器,要实现一个迭代器块。如果GetEnumerator方法的返回类型是非泛型接口,那么迭代器块的生成类型(yield type)是object,否则就是泛型接口的类型参数。
通过IL代码可以看到,对于yield return语句语句,编译器为我们生成了一个嵌套的类型(nested type) <GetEnumerator>d__0,并且这个类实现了IEnumerator接口。
当编译器遇到迭代块时,它创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。这个类有点类似与C# 1.0中手写的那段代码,它将所有需要记录的状态保存为实例变量。为了实现一个迭代器,这个状态机需要按顺序执行的操作:
- 它必须具有某个初始状态
- 当MoveNext被调用时,他需要执行GetEnumerator方法中的代码来准备下一个待返回的数据
- 当调用Current属性是,它必须返回上一个生成的数据
- 需要知道什么时候迭代结束,MoveNext会返回false
注意,当我们想要避免迭代器中的装箱和拆箱是,就要实现迭代器的泛型版本,由于泛型IEnumerable <T>接口继承了泛型型IEnumerable接口,我们需要在泛型迭代器代码中加入
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
这样,非泛型方法转而调用泛型方法,从而不需要再去实现非泛型的IEnumerable接口了。
迭代器的工作流程
前面简单提到了迭代器的工作流程,下面我们通过一个例子进一步看看迭代器工作流程。
class Program { static readonly String Padding = new String(' ', 30); static IEnumerable<Int32> CreateEnumerable() { Console.WriteLine("{0}Start of CreateEnumerable", Padding); for (int i = 0; i < 3; i++) { Console.WriteLine("{0}About to yield {1}", Padding, i); yield return i; Console.WriteLine("{0}After yield", Padding); } Console.WriteLine("{0}Yielding final value", Padding); yield return -1; Console.WriteLine("{0}End of CreateEnumerable()", Padding); } static void Main(string[] args) { IEnumerable<Int32> iterable = CreateEnumerable(); IEnumerator<Int32> iterator = iterable.GetEnumerator(); Console.WriteLine("Starting to iterate"); while (true) { Console.WriteLine("Calling MoveNext()..."); Boolean result = iterator.MoveNext(); Console.WriteLine("...MoveNext result={0}", result); if (!result) { break; } Console.WriteLine("Fetching Current..."); Console.WriteLine("...Current result={0}", iterator.Current); } Console.Read(); } }
一般迭代器都会结合foreach语句,然后foreach会在最后调用Dispose方法。这里为了演示,代码中使用while语句实现循环。
稍微打断一下,插入一个内容的介绍,通常为了实现IEnumerable,我们只会返回IEnumerator;如果仅仅是在方法中生成一个序列,可以返回IEnumerable。所以将代码改为下面的方式也可以工作:
static IEnumerator<Int32> CreateEnumerable() { …… } …… //IEnumerable<Int32> iterable = CreateEnumerable(); IEnumerator<Int32> iterator = CreateEnumerable();
两种方式的IL代码是不同的,这里只列出了编译器内嵌类型实现了那些接口,更详细的内容可以通过ILSpy查看:
返回IEnumerable
.class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0' extends [mscorlib]System.Object implements class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>, [mscorlib]System.Collections.IEnumerable, class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>, [mscorlib]System.Collections.IEnumerator, [mscorlib]System.IDisposable {……}
返回IEnumerator
.class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0' extends [mscorlib]System.Object implements class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>, [mscorlib]System.Collections.IEnumerator, [mscorlib]System.IDisposable {……}
回到这个例子,程序的输出结果为:
在这段代码中有几个注意点:
- 直到第一次调用MoveNext,CreateEnumerable中的方法才被调用
- 在调用MoveNext的时候,已经做好了所有操作,获取Current属性并没有执行任何代码
- 代码在yield return之后就停止执行,在下一次调用MoveNext方法的时候继续执行
- 同一个方法的不同地方可以有多个yield return语句
- 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用来结束方法的执行
第一点尤为重要:这意味着如果在方法调用时需要立即执行,就不能使用迭代器块。例如如果将参数验证放在迭代块中,那么他将不能够很好的起作用,这是经常会导致的错误的地方,而且这种错误不容易发现。
更多详情见请继续阅读下一页的精彩内容:
|
评论暂时关闭