深入理解C#(十三)

*第六章(C# 2:实现迭代器的捷径)

知识简介

迭代器模式:行为模式的一种范例,允许访问一个数据项序列中的所有元素,而不用关心序列的类型。能有效构建一个数据管道。(数据项序列进入数据管道后,经过一系列不同的转换或过滤后再从另一端出来)

行为模式:一种简化对象之间通信的设计模式

迭代器模式通过IEnumerator和IEnumerable接口以及它们的泛型等价物来封装。若某个类型实现了IEnumerable接口,就意味着它可以被迭代访问。

难点在于怎么自建迭代器

C# 2:利用yield语句简化迭代器

利用C# 2和yield return来迭代实例集合:

1
2
3
4
5
6
public IEnumerator GetEnumerator()
{
for(int index=0;index<values.Length;index++)
{ yield return values[(index+startingPoint)%values.Length];
}
}

这是一个实现迭代器块的方法,这个方法被声明为返回一个
IEnumerator接口,所以就只能使用迭代器块来实现返回类型为IEnumerable、IEnumerator等的方法。如果方法声明的返回类型是非泛型接口,那么迭代器块的生成类型(yield type)是object,否则就是泛型接口的类型参数。如:方法声明返回IEnumerable,就会得到string类型的生成类型。

在迭代器块中存在try/catch或try/finally代码块时,不能在try和finally中使用yield return

编写迭代器块代码实际上是在请求编译器创建一个状态机

当编译器看到迭代器块时,会为状态机创建一个嵌套类型,来正确记录块中的位置,以及局部变量(包括参数)的值。

这个状态机实现一下功能:

  • 必须具有某个初始状态
  • 每次调用MoveNext,提供下一个值之前(执行到yield return语句之前),执行GetEnumerator方法中的代码
  • 使用Current属性时,返回生成的上一个值
  • 知道何时完成生成值的操作,以便MoveNext返回false

迭代器的工作流程

  • 在第一次调用MoveNext之前,CreateEnumerable不会被调用
  • 所有工作在调用MoveNext时就完成了,获取Current的值,不执行任何代码
  • 在yield return的位置,代码就停止执行,在下一次调用MoveNext时又继续执行
  • 在一个方法中的不同地方可以编写多个yield return语句
  • 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用来结束

进一步了解

yield return语句临时推出了方法,知道再次调用MoveNext后继续执行,根本没有检查finally代码块的行为。

迭代器块不能实现具有ref或out参数的方法

yield break结束迭代器的执行,类似于普通方法中的return语句

finally在迭代器块中常用于释放资源,通常与using语句配合使用

迭代器示例

迭代时刻表中的日期

C# 1

1
2
3
for(DataTime day=timetable.StartDate;
day<=timetable.EndDate;
day=day.AddDays(1))

C# 2
​ foreach(DateTime day in timetable.DateRange)
想要手动实现迭代器很麻烦,但用迭代器块就很方便。为表示时刻表的类添加一个属性:

1
2
3
4
5
6
7
8
9
10
11
12
public IEnumerable<DateTime> dateRange
{
get
{
for(DateTime day=StartDate;
day<=EndDate;
day=day.AddDays(1))
{
yield return day;
}
}
}

迭代文件中的行

在.NET 4中,框架提供了reader.ReadLines来实现,如何自己轻松实现?
C# 1

1
2
3
4
5
6
7
8
using(TextReader reader=File.OpenText(filename))
{
string line;
while((line=reader.ReadLine()) != null)
{
//针对line进行操作
}
}
  • 获取TextReader
  • 管理TextReader的生命周期(using)
  • 迭代TextReader.ReadLine返回的行
  • 对这些行处理

生命周期管理和迭代机制都是样板代码,有两种方法改进。

一。使用委托,编写一个工具方法,将阅读器和委托作为参数,为文件中的每一行调用该委托,最后关闭阅读器。

二。使用迭代器一次返回文件中的一行,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static IEnumerable<string> ReadLines(string filename)
{
using (TextReader reader = File.OpenText(filename))
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}

foreach (string line in ReadLines(@"文件路径"))
{
Console.WriteLine(line);
}

进一步改进:如果我们想从网络流中读取文本或使用UTF-8以外的编码格式,最简单的想法是重新修改方法签名,使其接受一个TextReader。但这个方案很糟糕,问题是,如果在第一次调用MoveNext()之前发生了异常,就没有机会清理了。另外,若GetEnumerate人()被调用两次,但它们使用相同的阅读器。

1
2
3
4
5
static IEnumerable<String> ReadLines(Func <TextReader> provider)
{
using (TextReader reader=provider())
{
。。。

使用迭代器块和谓词对进行延迟过滤

看过linq后回来重看

CCR实现伪同步代码

CCR(Concurrency and Coordination Runtime,并发和协调运行时)
看完异步开发回来看