C# in depth (第六章 实现迭代器的捷径)
- 迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式。这是一种非常易于理解和使用的模式。实际上,它允许你访问一个数据项序列中的所有元素,而无须关心序列是什么类型----数组,列表,链表或任何其他类型。它能非常有效地构建出一个数据管道,经过一系列不同的转换或过滤后再从管道的另一端出来。实际上,这也是LINQ的核心模式之一。
- 可以将迭代器想象成数据库的游标,即序列中的某个位置。迭代器只能在序列中向前移动,而且对于同一个序列可能同时存在多个迭代器操作。
6.1 C#1.0:手写迭代器的痛苦
class IterationSample : IEnumerable { object[] values; int startingPoint; public IterationSample(object[] values, int startingPoint) { this.values = values; this.startingPoint = startingPoint; } public IEnumerator GetEnumerator() { return new IterationSampleIterator(this); } class IterationSampleIterator : IEnumerator { IterationSample parent; //1.正在迭代的集合 int position; //2.指出遍历到的位置 internal IterationSampleIterator(IterationSample parent) { this.parent = parent; position = -1; //3.在第一个元素之前开始 } public bool MoveNext() { if (position != parent.values.Length)//4.如果仍要遍历,那么增加position的值 { position++; } return position < parent.values.Length; } public object Current { get { if (position == -1 || position == parent.values.Length)//5.防止访问第一个元素之前和最后一个元素之后 { throw new InvalidOperationException(); } int index = (position + parent.startingPoint); //6.实现封装 index = index % parent.values.Length; return parent.values[index]; } } public void Reset() { position = -1; //7.返回第一个元素之前 } } static void Main() { object[] values = { "a", "b", "c", "d", "e" }; IterationSample collection = new IterationSample(values, 3); foreach (object x in collection) { Console.WriteLine(x); } } }
- 首先要知道,我们需要在某个地方存储某个状态。迭代器模式的一个重要方面就是,不用一次返回所有数据----调用代码一次只需获取一个元素。这意味着我们需要确定访问到了数组中的哪个位置。在了解C#2.0编译器为我们所做的事情时,迭代器的这种状态特制十分重要,因此,要密切关注本例中的状态。
- 我们要记住迭代的原始值的集合 1
- 并用简单的从零开始的数组跟踪我们所在的位置 2
- 为了返回元素,要根据原始点对索引进行偏移。6
- 要让迭代器逻辑上从第一个元素的位置之前开始 3
- 所以在第一次使用Current属性之前,调用代码必须调用MoveNext方法。4 中的条件增量可以保证 5中的条件判断简单准确,即使在程序第一次报告无可用数据后又调用MoveNext也没有问题。
- 为了重置迭代器,我们将我们的逻辑位置设置回"第一个元素之前“ 7
6.2.1 迭代器和yield return 简介
class IteratorBlockIterationSample : IEnumerable { object[] values; int startingPoint; public IteratorBlockIterationSample(object[] values, int startingPoint) { this.values = values; this.startingPoint = startingPoint; } public IEnumerator GetEnumerator() { for (int index = 0; index < values.Length; index++) { yield return values[(index + startingPoint) % values.Length]; //完全替换掉了整个IterationSampleIterat } } static void Main() { object[] values = { "a", "b", "c", "d", "e" }; IteratorBlockIterationSample collection = new IteratorBlockIterationSample(values, 3); foreach (object x in collection) { Console.WriteLine(x); } } }
- 这个方法不是一个普通的方法,而是实现一个迭代器块的方法。这个方法被声明为返回一个IEnumerator接口。
- 当编译器看到迭代器块时,会为状态机创建一个嵌套类型,来正确记录块中的位置以及局部变量(包括参数)的值。所创建的类类似于我们之前用普通方法实现的类,用实例变量来保存所有必要的状态。下面来看一下,要实现迭代器,这个状态机要做哪些事情:
- 它必须具有某个初始状态:
- 每次调用MoveNext时,在提供下一个值之前(换句话说,就是执行到yield return 语句之前),它需要执行GetEnumerator方法中的代码
- 使用Current属性时,它必须返回我们生成的上一个值。
- 它必须知道何时完成生成值的操作,以便MoveNext返回False。
6.2.2 观察迭代器的工作流程
class IteratorWorkflow { static readonly string Padding = new string(' ', 30); static IEnumerable<int> 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 GetEnumerator()", Padding); } static void Main() { IEnumerable<int> iterable = CreateEnumerable(); IEnumerator<int> iterator = iterable.GetEnumerator(); Console.WriteLine("Starting to iterate"); while (true) { Console.WriteLine("Calling MoveNext()..."); bool result = iterator.MoveNext(); Console.WriteLine("... MoveNext result={0}", result); if (!result) { break; } Console.WriteLine("Fetching Current..."); Console.WriteLine("... Current result={0}", iterator.Current); } } }
- 在第一次调用MoveNext之前,CreateEnumerable中的代码不会被调用;
- 所有工作在调用MoveNext时就完成了,获取Current的值不会执行任何代码;
- 在yield return的位置, 代码就停止执行,在下一次调用MoveNext时又继续执行;
- 在一个方法中的不同地方可以编写多个yield return语句;
- 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用结束来结束方法的执行。
6.2.3 进一步了解迭代器执行流程
- 使用yield break结束迭代器的执行 (如果你希望”提早退出“,那么yield break 正是你所需要的。它实际上终止了迭代器的运行,让当前对MoveNex的调用返回false。
class YieldBreak { static IEnumerable<int> CountWithTimeLimit(DateTime limit) { for (int i = 1; i <= 100; i++) { if (DateTime.Now >= limit) { yield break; } yield return i; } } static void Main() { DateTime stop = DateTime.Now.AddSeconds(2); foreach (int i in CountWithTimeLimit(stop)) { Console.WriteLine("Received {0}", i); Thread.Sleep(300); } } }
2. finally 代码块的执行
static IEnumerable<int> CountWithTimeLimit(DateTime limit) { try { for (int i = 1; i <= 100; i++) { if (DateTime.Now >= limit) { yield break; } yield return i; } } finally { Console.WriteLine("Stopping!"); } } static void Main() { DateTime stop = DateTime.Now.AddSeconds(2); foreach (int i in CountWithTimeLimit(stop)) { Console.WriteLine("Received {0}", i); Thread.Sleep(300); } }
- finally会在yield break之后执行
static IEnumerable<int> CountWithTimeLimit(DateTime limit) { try { for (int i = 1; i <= 100; i++) { if (DateTime.Now >= limit) { yield break; } yield return i; } } finally { Console.WriteLine("Stopping!"); } } static void Main() { DateTime stop = DateTime.Now.AddSeconds(2); foreach (int i in CountWithTimeLimit(stop)) { Console.WriteLine("Received {0}", i); if (i > 3) { Console.WriteLine("Returning"); return; } Thread.Sleep(300); } }
- finally会在Main函数里的return之后执行;只要调用者使用了foreach循环,迭代器块中的finally将按照你期望的方式工作。
6.2.4具体实现中的奇特之处
6.3 真实的迭代器示例
6.3.1迭代时刻表中的日期
for (DateTime day = timetable.StartDate; day <= timetable.EndDate; day= day.AddDays(1)) { }
public IEnumerable<DateTime> DateRange { get { for(DateTime day = StartDate; day<=EndDate; day =day.AddDays(1)) { yield return day; } } }
6.3.2 迭代文件中的行
class LineReader { public static IEnumerable<string> ReadLines(string filename) { using (TextReader reader = File.OpenText(filename)) { string line; while ((line = reader.ReadLine()) != null) { yield return line; } } } static void Main() { foreach (string line in ReadLines("../../LineReader.cs")) { Console.WriteLine(line); } } }
6.3.3 使用迭代器块和谓词对项进行延迟过滤
class FakeLinq { public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate) { if (source == null || predicate == null)//1.热情的检查参数 { throw new ArgumentNullException(); } return WhereImpl(source, predicate); //2.懒惰地(延时)处理数据 } private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate) { foreach (T item in source) { if (predicate(item)) //3.检查当前项与谓词是否匹配 { yield return item; } } } static void Main() { IEnumerable<string> lines = LineReader.ReadLines("../../FakeLinq.cs"); Predicate<string> predicate = delegate(string line) { return line.StartsWith("using"); }; foreach (string line in Where(lines, predicate)) { Console.WriteLine(line); } } }
假设将所有的内容都放入同一个方法中,那么调用Where<string>(null,null)时会发生什么呢? 答案是什么也不发生,或至少不会抛出我们认为的异常。这是由迭代器块的延迟语义所决定的。因为在第一次调用MoveNext()之前,不会执行方法体内的任何代码。所以是像代码清单6-9中那样将方法一分为二。
- 首先在普通方法中检查参数 1.
- 然后调用使用迭代器块实现的方法,在得到请求时再延时处理数据2.
- 迭代器块本身十分简单:对于原始集合中的每一项,我们测试谓词3.