yield 迭代器 转 一篇
这个系列的主要目的是尽量能覆盖C# 1.1之后的语法更新,以便让大家能够熟悉C# 2.0到4.0的语法特性,以提高编程效率,这里我忽略了一些诸如泛型、LINQ等等需要大章节才能阐述清楚的东西,原因是关注这些知识点的文章比比皆是,我所要写的语法都是一些比较小的,容易被人所忽略的地方。如果我有任何遗漏或者错误的地方,请给我个站内消息
1. 迭代器 适用范围:c# 2.0之后、
C# 1.0开始提供了非常方便的foreach循环,只要一个数据集实现了一个无参数的,返回Enumerator的GetEnumerator方法(甚至不需要实现任何接口),就可以在foreach循环中遍历数据集中的每个元素。大部分情况下,我们将数据存入某个Collection类,利用Collection类的GetEnumerator方法,就可以很方便地使用Foreach循环。
但有些时候我们必须自己去实现Enumerator,比如说打印某年每个月的天数,为了体现OOP的原则,我们的程序逻辑(即对闰年的判断,很高兴C#提供了相关的方法)应该被封装起来。
- using System;
- using System.Collections;
- namespace SharpDemo{
- publicclass DaysOfTheMonth
- {
- public DaysOfTheMonth(int year)
- {
- this.year = year;
- }
- int year = 1900;
- public System.Collections.IEnumerator GetEnumerator()
- {
- returnnew DaysOfMonthEnumrator(this.year);
- }
- class DaysOfMonthEnumrator : IEnumerator
- {
- privateint[] days = newint[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
- public DaysOfMonthEnumrator(int year)
- {
- if (DateTime.IsLeapYear(year))
- {
- days[1] = 29;
- }
- }
- privateint index = -1;
- public IEnumerator GetEnumerator()
- {
- returnthis;
- }
- publicvoid Reset()
- {
- index = -1;
- }
- public object Current
- {
- get
- {
- if (this.index < this.days.Length)
- {
- returnthis.days[this.index];
- }
- else
- {
- thrownew IndexOutOfRangeException();
- }
- }
- }
- public bool MoveNext()
- {
- if (this.days.Length == 0)
- {
- returnfalse;
- }
- else
- {
- this.index += 1;
- if (this.index == this.days.Length)
- {
- returnfalse;
- }
- else
- {
- returntrue;
- }
- }
- }
- }
- }
- }
using System; using System.Collections; namespace SharpDemo{ public class DaysOfTheMonth { public DaysOfTheMonth(int year) { this.year = year; } int year = 1900; public System.Collections.IEnumerator GetEnumerator() { return new DaysOfMonthEnumrator(this.year); } class DaysOfMonthEnumrator : IEnumerator { private int[] days = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; public DaysOfMonthEnumrator(int year) { if (DateTime.IsLeapYear(year)) { days[1] = 29; } } private int index = -1; public IEnumerator GetEnumerator() { return this; } public void Reset() { index = -1; } public object Current { get { if (this.index < this.days.Length) { return this.days[this.index]; } else { throw new IndexOutOfRangeException(); } } } public bool MoveNext() { if (this.days.Length == 0) { return false; } else { this.index += 1; if (this.index == this.days.Length) { return false; } else { return true; } } } } } }
这段代码较长但不难理解,它相当好地封装了程序逻辑,调用起来也相当简洁优雅:
- DaysOfTheMonth enu = new DaysOfTheMonth(1981);
- foreach( int days in enu)
- Console.Out.WriteLine(days);
DaysOfTheMonth enu = new DaysOfTheMonth(1981); foreach( int days in enu) Console.Out.WriteLine(days);
我们也看到实现Enumerator的过程未免过于复杂,一旦我们需要多个Enumerator来进行逆序,正序,奇偶数序迭代,代码就会相当繁杂。
C# 2.0之后新的yield关键字使得这个过程变得轻松简单。
- publicclass DaysOfMonth
- {
- int year = 1900;
- int[] days = newint[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
- public DaysOfMonth2(int year)
- {
- this.year = year;
- if (DateTime.IsLeapYear(year))
- {
- days[1] = 29;
- }
- }
- public IEnumerator GetEnumerator()
- {
- for (int i = 0; i < this.days.Length; i++)
- {
- yield returnthis.days[i];
- }
- }
- }
public class DaysOfMonth { int year = 1900; int[] days = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; public DaysOfMonth2(int year) { this.year = year; if (DateTime.IsLeapYear(year)) { days[1] = 29; } } public IEnumerator GetEnumerator() { for (int i = 0; i < this.days.Length; i++) { yield return this.days[i]; } } }
这就是C# 2.0 中的迭代器,它使您能够方便地在类或结构中支持 foreach 迭代,而不必实现整个 IEnumerable 接口。
迭代器的返回类型必须为 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>。当编译器检测到迭代器时,它将自动生成 IEnumerable 或 IEnumerable<T> 接口的 Current、MoveNext 和 Dispose 方法。
yield 关键字用于指定返回的值。到达 yield return 语句时,会保存当前位置。下次调用迭代器时将从此位置重新开始执行。而yield break则可以中止迭代,下面的代码有助于理解这点。
- publicclass CityCollection : IEnumerable<string>
- {
- public IEnumerator<string> GetEnumerator()
- {
- yield return"New York";
- yield return"Paris";
- yield return"London";
- }
- }
public class CityCollection : IEnumerable<string> { public IEnumerator<string> GetEnumerator() { yield return "New York"; yield return "Paris"; yield return "London"; } }
会依次打印出"New York", "Paris" "London". 有时候迭代器与迭代一起可以产生奇妙的效果,但也会耗费大量的内存。
- IEnumerable<T> ScanInOrder(Node<T> root)
- {
- if(root.LeftNode != null)
- {
- foreach(T item in ScanInOrder(root.LeftNode))
- {
- yield return item;
- }
- }
- yield return root.Item;
- if(root.RightNode != null)
- {
- foreach(T item in ScanInOrder(root.RightNode))
- {
- yield return item;
- }
- }
- }
IEnumerable<T> ScanInOrder(Node<T> root) { if(root.LeftNode != null) { foreach(T item in ScanInOrder(root.LeftNode)) { yield return item; } } yield return root.Item; if(root.RightNode != null) { foreach(T item in ScanInOrder(root.RightNode)) { yield return item; } } }
foreach语句会隐式地调用集合的无参的GetEnumerator方法來得到一個迭代器。一个集合类中只能定义一个这样的无参的GetEnumerator方法,不过我们也可以在类中实现多个迭代器,但每个迭代器都必须像任何类成员一样有唯一的名称。
- using System.Collections.Generic;
- publicclass Stack<T>: IEnumerable<T>
- {
- T[] items;
- int count;
- publicvoid Push(T data) {...}
- public T Pop() {...}
- public IEnumerator<T> GetEnumerator() {
- for (int i = count – 1; i >= 0; --i) {
- yield return items[i];
- }
- }
- public IEnumerable<T> TopToBottom {
- get {
- returnthis;
- }
- }
- public IEnumerable<T> BottomToTop {
- get {
- for (int i = 0; i < count; i++) {
- yield return items[i];
- }
- }
- }
- }
using System.Collections.Generic; public class Stack<T>: IEnumerable<T> { T[] items; int count; public void Push(T data) {...} public T Pop() {...} public IEnumerator<T> GetEnumerator() { for (int i = count – 1; i >= 0; --i) { yield return items[i]; } } public IEnumerable<T> TopToBottom { get { return this; } } public IEnumerable<T> BottomToTop { get { for (int i = 0; i < count; i++) { yield return items[i]; } } } }
TopToBottom属性的get访问器只返回this,因为Stack本身就是一个可枚举类型。BottomToTop屬性使用C#迭代器返回了另一个可枚举接口。
实现IEnumerable接口的类看起来非常象一个枚举器工厂类,每次都产生一个独立的IEnumerator类。
- using System;
- using System.Collections.Generic;
- class Test
- {
- static IEnumerable<int> FromTo(int from, int to) {
- while (from <= to) yield return from++;
- }
- staticvoid Main() {
- IEnumerable<int> e = FromTo(1, 10);
- foreach (int x in e) {
- foreach (int y in e) {
- Console.Write("{0,3} ", x * y);
- }
- Console.WriteLine();
- }
- }
- }
using System; using System.Collections.Generic; class Test { static IEnumerable<int> FromTo(int from, int to) { while (from <= to) yield return from++; } static void Main() { IEnumerable<int> e = FromTo(1, 10); foreach (int x in e) { foreach (int y in e) { Console.Write("{0,3} ", x * y); } Console.WriteLine(); } } }
上面的代码打印了一个从1到10的乘法表。注意FromTo方法只被调用了一次用来产生实现了IEnumerable接口的变量e。而e.GetEnumerator()被调用了多次(通过foreach语句)來产生多个相同的迭代器。這些迭代器都封裝了FromTo声明中指定的代碼。注意,迭代其代码的时候改变了from参数。但是,迭代器是独立的,因此对于from参数和to参数,每個迭代器有它自己的一份拷贝。