深入理解C#(第3版)-- 【C#2】第6章 实现迭代器的捷径(学习笔记)
迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式。
如果某个类型实现了IEnumerable 接口,就意味着它可以被迭代访问。调用GetEnumerator 方法将返回IEnumerator 的实现,这就是迭代器本身。
6.1 C# 1:手写迭代器的痛苦
迭代器模式的一个重要方面就是,不用一次返回所有数据——调用代码一次只需获取一个元素。
6.2 C# 2:利用yield语句简化迭代器
6.2.1 迭代器块和yield return简介
代码清单6-4 利用C# 2 和yield return来迭代示例集合
public IEnumerator GetEnumerator() { for (int index = 0; index < values.Length; index++) { yield return values[(index + startingPoint) % values.Length]; } }
在你看到yield return 之前,这个方法看上去一直都非常正常。这句代码就是告诉C#编译器,这个方法不是一个普通的方法,而是实现一个迭代器块的方法。
如果方法声明的返回类型是非泛型接口,那么迭代器块的生成类型(yield type )是object,否则就是泛型接口的类型参数。例如,如果方法声明为返回IEnumerable<string>,那么就会得到string类型的生成类型。
在迭代器块中不允许包含普通的return 语句——只能是yield return。在代码块中,所有yield return语句都必须返回和代码块的生成类型兼容的值。在之前的例子中,不能在一个声明返回IEnumerable<string>的方法中编写yield return 1;这样的代码。
对yield return的限制
如果存在任何catch代码块,则不能在try 代码块中使用yield return,并且在finally代码块中也不能使用yield return或yield break(这个语句马上就要讲到)。
当编译器看到迭代器块时,会为状态机创建一个嵌套类型,来正确记录块中的位置以及局部变量(包括参数)的值。这个状态机要做的事情:
它必须具有某个初始状态;
每次调用MoveNext 时,在提供下一个值之前(换句话说,就是执行到yield return语句之前),它需要执行GetEnumerator 方法中的代码;
使用Current属性时,它必须返回我们生成的上一个值;
它必须知道何时完成生成值的操作,以便MoveNext 返回false。
yield return语句只表示“暂时地”退出方法——事实上,你可把它当作暂停。
6.2.2 观察迭代器的工作流程
代码清单6-5 显示迭代器及其调用者之间的调用序列
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 CreateEnumerable()", Padding); } ... 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 进一步了解迭代器执行流程
1. 使用yield break 结束迭代器的执行
2. finally代码块的执行
代码清单6-7 与try/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!"); //不管循环是否结束都执行 } } ... DateTime stop = DateTime.Now.AddSeconds(2); foreach (int i in CountWithTimeLimit(stop)) { Console.WriteLine("Received {0}", i); Thread.Sleep(300); }
把代码清单6-7的“调用”部分改为下面这样会怎么样:
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); }
在这里,我们不是提前停止执行迭代器代码,而是提前停止使用迭代器。输出结果也许令人感到意外:
Received 1
Received 2
Received 3
Received 4
Returning
Stopping!
此处,在foreach循环中的return 语句执行后,迭代器的finally代码也被执行了。
foreach会在它自己的finally代码块中调用IEnumerator 所提供的Dispose方法(就像using 语句)。
6.2.4 具体实现中的奇特之处
在第一次调用MoveNext 之前,Current属性总是返回迭代器产生类型的默认值;
在MoveNext 返回false之后,Current属性总是返回最后的生成值;
Reset总是抛出异常,而不像我们手动实现的重置过程那样,为了遵循语言规范,这是必要的行为;
嵌套类总是实现IEnumerator 的泛型形式和非泛型形式(提供给泛型和非泛型的IEnumerable 所用)。
6.3 真实的迭代器示例
6.3.1 迭代时刻表中的日期
6.3.2 迭代文件中的行
代码清单6-8 使用迭代器块循环遍历文件中的行
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("test.txt")) { Console.WriteLine(line); }
static IEnumerable<string> ReadLines(Func<TextReader> provider) { using(TextReader reader = provider()) { string line; while((line = reader.ReadLine()) != null) { yield return line; } } }
static IEnumerable<string> ReadLines(string filename) { return ReadLines(filename, Encoding.UTF8); } static IEnumerable<string> ReadLines(string filename, Encoding encoding) { return ReadLines(delegate { return File.OpenText(filename, encoding); }); }
6.3.3 使用迭代器块和谓词对项进行延迟过滤
代码清单6-9 使用迭代器块实现LINQ的Where方法
public static IEnumerable<T> Where<T>(IEnumerable<T> source,Predicate<T> predicate) { if (source == null || predicate == null) //热情地检查参数 { thrownew ArgumentNullException(); } return WhereImpl(source, predicate); //懒惰地处理数据 } private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source,Predicate<T> predicate) { foreach (T item in source) { if (predicate(item)) //检查当前项与谓词是否匹配 { yield return item; } } } ... 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) 时会发生什么呢?答案是什么也不会发生,或至少不会抛出我们认为的异常。这是由迭代器块的延迟语义所决定的。
6.4 使用CCR实现伪同步代码
CCR (Concurrency and Coordination Runtime ,并发和协调运行时)Microsoft Robotics
HoldingsValue ComputeTotalStockValue(string user, string password) { Tokentoken = AuthService.Check(user, password); Holdings stocks = DbService.GetStockHoldings(token); StockRates rates = StockService.GetRates(token); return ProcessStocks(stocks, rates); }
实际上可以让你在这个代码块所涉及流程的特定位置“暂停”当前执行过程,并随后携带着同样的状态返回同一位置。
连续传递(continuation-passing)
通过为CRR 提供一个IEnumerator<ITask>(这里ITask是一个由CCR 定义的接口)实现来完成这个操作。
异步开发中两个复杂的问题就是处理状态和在感兴趣的事情发生之前进行有效的暂停。
浙公网安备 33010602011771号