迭代器
C# foreach 语句用于循环访问可枚举 (enumerable) 集合的元素。为了成为可枚举的类型,集合必须具有返回枚举器 (enumerator) 的无参数的 GetEnumerator 方法。一般而言,枚举器不易实现,但是通过使用迭代器可以显著简化该任务。
迭代器 (iterator) 是一个产生 (yield) 有序值序列的语句块。迭代器与普通语句块的区别在于迭代器存在一个或多个 yield 语句:
· yield return 语句产生迭代的下一个值。
· yield break 语句指示迭代完成。
只要函数成员的返回类型是枚举器接口 (enumerator interface) 之一或可枚举接口 (enumerable interface) 之一,迭代器就可用作该函数成员的函数体:
· 枚举器接口为 System.Collections.IEnumerator 和从 System.Collections.Generic.IEnumerator<T> 构造的类型。
· 可枚举接口为 System.Collections.IEnumerable 和从 System.Collections.Generic.IEnumerable<T> 构造的类型。
迭代器并非一种成员,而是一种实现函数成员的手段,了解这一点很重要。通过迭代器实现的成员能够被其他可能使用也可能不使用迭代器实现的成员重写或重载。
下面的 Stack<T> 类使用一个迭代器实现其 GetEnumerator 方法。该迭代器以自顶向下的顺序枚举堆栈的元素。
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];
}
}
}
GetEnumerator 方法的存在使得 Stack<T> 成为可枚举类型,这样,我们就可以在 foreach 语句中使用 Stack<T> 的实例。下面的示例将值 0 至 9 推入一个整数堆栈,然后使用 foreach 循环按自顶向下的顺序显示这些值。
using System;
class Test
{
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack) Console.Write("{0} ", i);
Console.WriteLine();
}
}
该示例的输出为:
9 8 7 6 5 4 3 2 1 0
foreach 语句隐式调用集合的无参数 GetEnumerator 方法获得枚举器。一个集合只能定义一个这样的无参数 GetEnumerator 方法,但是可以有多种枚举方式以及多种通过参数控制枚举的方式。在这种情况下,集合可以使用迭代器实现多个返回可枚举接口类型之一的属性或方法。例如,Stack<T> 可引入 IEnumerable<T> 类型的两个新属性 TopToBottom 和 BottomToTop:
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,因为该堆栈本身是可枚举的。BottomToTop 属性返回一个使用 C# 迭代器实现的可枚举接口类型。下面的示例显示如何使用这两个属性以任一顺序枚举堆栈元素:
using System;
class Test
{
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack.TopToBottom) Console.Write("{0} ", i);
Console.WriteLine();
foreach (int i in stack.BottomToTop) Console.Write("{0} ", i);
Console.WriteLine();
}
}
当然,这些属性也可以在 foreach 语句之外使用。下面的示例将调用这些属性的结果传递给一个单独的 Print 方法。该示例还演示了一个用作 FromToBy 方法(该方法带有参数)的方法体的迭代器:
using System;
using System.Collections.Generic;
class Test
{
static void Print(IEnumerable<int> collection) {
foreach (int i in collection) Console.Write("{0} ", i);
Console.WriteLine();
}
static IEnumerable<int> FromToBy(int from, int to, int by) {
for (int i = from; i <= to; i += by) {
yield return i;
}
}
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
Print(stack.TopToBottom);
Print(stack.BottomToTop);
Print(FromToBy(10, 20, 2));
}
}
该示例的输出为:
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
10 12 14 16 18 20
泛型和非泛型可枚举接口仅含有一个成员,即一个不带参数并返回一个枚举器接口类型的 GetEnumerator 方法。可枚举接口起到“枚举器工厂”的作用。如果正确实现了可枚举接口,每次调用它们的 GetEnumerator 方法时,都会生成一个独立的枚举器。假设在两次调用 GetEnumerator 期间可枚举接口类型的内部状态没有改变,则所返回的两个枚举器应以相同的顺序产生相同的枚举值集合。即使枚举器的生存期重叠,这个结论也应该有效,如下面的代码示例所示:
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 方法仅被调用一次来生成可枚举的 e。但是,e.GetEnumerator() 被调用多次(由 foreach 语句调用)以生成多个等效的枚举器。这些枚举器全都封装了在 FromTo 的声明中指定的迭代器代码。注意该迭代器代码将修改 from 参数。然而,这些枚举器独立工作,因为每个枚举器都会获得它自己的 from 和 to 参数的副本。枚举器之间的过渡状态的共享是若干常见细微缺陷之一,在实现可枚举接口和枚举器时应该避免。C# 迭代器旨在帮助避免这些问题,并以简单、直观的方法实现可靠的可枚举接口和枚举器。