您的位置:首页 > 编程语言 > C#

C# in depth (第六章 实现迭代器的捷径)

2015-08-14 16:56 579 查看
迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式。这是一种非常易于理解和使用的模式。实际上,它允许你访问一个数据项序列中的所有元素,而无须关心序列是什么类型----数组,列表,链表或任何其他类型。它能非常有效地构建出一个数据管道,经过一系列不同的转换或过滤后再从管道的另一端出来。实际上,这也是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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: