4.1 迭代器块
一个迭代器块(iterator block)是一个能够产生有序的值序列的块。迭代器块和普通语句块的区别就是其中出现的一个或多个yield语句。
yield return语句产生迭代的下一个值。
yield break语句表示迭代完成。
只要相应的函数成员的返回值类型是一个枚举器接口(见4.1.1)或是一个可枚举接口(见4.1.2),一个迭代器块就可以用作方法体、运算符体或访问器体。
迭代器块并不是C#语法中的独立元素。它们受多种因素的制约,并且对函数成员声明的语义有很大影响,但在语法上它们只是块(block)。
当一个函数成员用一个迭代器块来实现时,如果函数成员的形式参数列表指定了ref或out参数,则会引起编译错误。
如果在迭代器块中出现了return语句,则会因此编译错误(但yield return语句是允许的)。
如果迭代器块包含不安全上下文,则会引起编译错误。一个迭代器块必须定义在一个安全的上下文中,即使它的声明嵌套在一个不安全上下文中。
4.1.1 枚举器接口
枚举器(enumerator)接口包括非泛型接口System.Collections.IEnumerator和泛型接口System.Collections.Generic.IEnumerator<T>的所有实例化。在这一章中,这些接口将分别称作IEnumerator和IEnumerator<T>。
4.1.2 可枚举接口
可枚举(enumerable)接口包括非泛型接口System.Collections.IEnumerable和泛型接口System.Collections.Generic.IEnumerable<T>。在这一章中,这些接口分别称作IEnumerable和IEnumerable<T>。
4.1.3 生成类型
一个迭代器块能够产生一个有序的值序列,其中所有的制具有相同的类型。这个类型称为迭代器块的生成类型(yield type)。
用于实现一个返回IEnumerator或IEnumerable的函数成员的迭代器块的生成类型为object。
用于是下一个返回IEnumerator<T>或IEnumerable<T>的函数成员的迭代器块的生成类型为T。
4.1.4 this访问
在一个类的一个实例成员中的迭代器块里,表达式this是一个值。这个值的类型就是出现这种用法的类,并且它是对被调用的方法所在的对象的一个引用。
在一个结构的一个实例成员中的迭代器块里,表达式this是一个变量。这个变量的类型就是出现这种用法的结构。这个变量存贮了对被调用成员所在结构的一个拷贝。结构的实例成员中的迭代器块里的this变量和以该结构为类型的值变量完全一样。
4.2 Enumerator对象
如果一个函数成员使用了迭代器块来返回一个枚举器接口类型,对该函数成员的调用不会立即执行迭代器块中的代码,而是建立并返回一个枚举器对象。这个对象封装了迭代器块中指定的代码,而对迭代器中指定的代码的执行发生在调用该枚举器对象的MoveNext()方法时。一个枚举器对象具有如下特征:
它实现了IEnumerator和IEnumerator<T>,这里T是迭代器块的生成类型。
它实现了System.IDisposable。
它用传递给函数成员的参数值(如果有的话)和实例值进行初始化。
它有四个可能的状态:before、running、suspended和after,其初始状态为before。
典型的枚举器对象是由编译器自动生成的封装了迭代器块中的代码并实现了枚举器接口的枚举器类的实例,但其他的实现也是允许的。如果一个枚举器类是由编译器自动生成的,则该类是直接或间接地嵌套在函数成员中的,具有私有的可访问性,并且具有一个由编译器保留使用的名字。
一个枚举器对象可以实现上面所述之外的其它接口。
后面的几节详细地描述了由一个枚举器对象所实现的IEnumerable和IEnumerable<T>接口中的MoveNext()、Current和Dispose()成员的确切行为。
注意,枚举器对象不支持IEnumerator.Reset()方法。调用该方法会抛出System.NotSupporteException异常。
4.2.1 MoveNext()方法
枚举器对象的MoveNext()方法封装了迭代器块的代码。对MoveNext()方法的调用执行了迭代器块中的代码,并为枚举器对象的Current属性设置一个适当的值。MoveNext()方法完成得确切动作取决于调用MoveNext()方法是枚举器对象的状态:
如果枚举器对象的状态为before,调用MoveNext()方法:
将状态设置为running。
将迭代器块对象的参数(包括this)初始化为枚举器对象初始化时所保存的变量值和实例值。
从执行迭代器块的开始执行,直到被中断(将在下面讨论)。
如果枚举器对象的状态为running,则调用MoveNext()方法的结果未指定。
如果枚举器对象的状态为suspended,调用MoveNext()方法:
将状态设置为running。
将所有局部变量和参数(包括this)恢复为迭代器块执行过程中最后一次挂起时所保存的值。注意这些变量所引用的对象的内容在上一次MoveNext()后可能会发生改变。
从上一次执行中断所在的yield return语句继续执行迭代器块,直到执行再次被中断(将在下面讨论)。
如果枚举器对象的状态为after,调用MoveNext()方法将返回false。
当MoveNext()方法执行迭代器块时,执行过程会通过四种途径中断:yield return语句、yield break语句、遇到迭代器块的结尾以及迭代器块中抛出了异常并被传播到块外。
当遇到yield return语句(见4.4)时:
对语句中给定的表达式进行求值,隐式转换为生成类型,并赋给枚举器对象的Current属性。
挂起迭代器体的执行过程。保存所有局部变量和参数(包括this)的值,以及这个yield return语句的位置。如果该yield return语句位于一个或多个try块中,则与之相关联的finally块在此时还不会被执行。
将枚举器对象的状态设置为suspended。
向MoveNext()方法的调用者返回true,表示迭代已经成功地转移到下一个值上。
当遇到yield break语句(见4.4)时:
如果该yield break语句位于一个或多个try块中,则执行与之相关联的finally块。
将枚举器对象的状态设置为after。
向MoveNext()方法的调用者返回false,表示迭代完成。
当遇到迭代器快的结尾时:
将枚举器对象的状态设置为after。
向MoveNext()方法的调用者返回false,表示迭代完成。
当迭代器快报出了一个异常,并传播到块外时:
迭代器块中适当的finally块将被执行。
将枚举器对象的状态设置为after。
将异常传播给MoveNext()方法的调用者。
4.2.2 Current属性
一个枚举器对象的Current属性受迭代器块中的yield return语句的影响。
当一个枚举器对象处于suspended状态时,Current属性的值由最后一次对MoveNext()方法的调用设置。当一个枚举器对象处于before、running或after状态时,访问Current属性的结果是未定义的。
如果一个迭代器块的生成类型不是object,通过枚举器对象实现的IEnumerable以及相应的IEnumerator<T>对Current的访问会将结果转换为object。
4.2.3 Dispose()方法
Dispose()方法通过将枚举器对象的状态设置为after来清除迭代器。
如果枚举器对象的状态为before,调用Dispose()方法将其状态设置为after。
如果枚举器对象的状态为running,调用Dispose()方法的结果是未定义的。
如果枚举器对象的状态为suspended,调用Dispose()方法:
将状态设置为running。
执行所有的finally块,好像yield return语句是yield break语句一样。如果这导致了异常被抛出并传播到迭代器块外,则将枚举器对象的状态设置为after并将异常传播给Dispose()方法的调用者。
将状态设置为after。
如果枚举器对象的状态为after,调用Dispose()方法没有任何效果。
4.3 Enumerable对象
当一个返回一个可枚举接口类型的函数成员使用了迭代器块时,对该函数成员的调用不会立即执行迭代器块中的代码,而是建立并返回一个可枚举对象。该可枚举对象有一个GetEnumerator()方法,能够返回一个枚举器对象。该枚举器对象封装了迭代器块中指定的代码,当调用这个枚举器对象的MoveNext()方法时,会执行迭代器块中的代码。一个可枚举对象具有如下特征:
它实现了IEnumerable或IEnumerable<T>,这里T是迭代器块的生成类型。
它用传递给函数成员的参数值(如果有的话)和实例值进行初始化。
典型的可枚举对象是由编译器自动生成的封装了迭代器块中的代码并实现了可枚举接口的可枚举类的实例,但其他的实现也是允许的。如果一个可枚举类是由编译器自动生成的,则该类是直接或间接地嵌套在函数成员中的,具有私有的可访问性,并且具有一个由编译器保留使用的名字。
一个可枚举对象可以实现上述之外的其它接口。例如,一个可枚举对象还可以实现IEnumerator和IEnumerator<T>,使得它既是可枚举的又是一个枚举器。这种情况下,当可枚举对象的GetEnumerator()方法第一次被调用时,将返回可枚举对象本身。以后对可枚举对象的GetEnumerator()方法的调用(如果有的话),将返回可枚举对象的一个拷贝。因此,每个被返回的枚举器具有其自己的状态,并且一个枚举器和其它枚举器互不影响。
4.3.1 GetEnumerator()方法
一个可枚举对象提供了对IEnumerator和IEnumberator<T>接口的GetEnumerator()方法的实现。两个GetEnumerator()方法共享一个实现,能够获取并返回一个有效的枚举器对象。该枚举器对象使用可枚举对象被初始化时所保存的参数值和实例值进行初始化,该枚举器对象的功能如4.2节所描述。
4.4 yield语句
迭代器块中的yield语句用于生成一个值,或发出一个迭代完成的信号。
embedded-statement:
...
yield-statement
yield-statement:
yield return expression ;
yield break ;
内嵌语句:
...
yield语句
yield语句:
yield return 表达式 ;
yield break ;
为了保证和现有程序的兼容性,yield并不是一个保留字,只有当一个return语句紧随其后时,yield语句才有这特殊的意义。其它情况下,yield语句可以用作标识符。
yield语句的出现首很多限制,如下所描述:
如果一个yield语句出现在方法体、运算符体或访问器体之外,则会引起编译错误。
如果一个yield语句出现在匿名方法内部,则会引起编译错误。
如果一个yield语句出现在finally或一个try块内,则会引起编译错误。
如果一个yield语句出现在一个带有catch语句的try块内,则会引起编译错误。
下面的例子展示了一些yield语句的有效的和无效的用法。
delegate IEnumerable<int> D();
IEnumerator<int> GetEnumerator() {
try {
yield return 1; // 正确
yield break; // 正确
}
finally {
yield return 2; // 错误,yield出现在finally块中E
yield break; // 错误,yield出现在finally块中
}
try {
yield return 3; // 错误,yield return语句出现在try...catch语句中
yield break; // 正确
}
catch {
yield return 4; // 错误,yield return语句出现在try...catch语句中
yield break; // 正确
}
D d = delegate {
yield return 5; // 错误,yield语句出现在匿名方法中
};
}
int MyMethod() {
yield return 1; // 错误,迭代器块具有错误的返回值类型
}
从yield return语句中的表达式的类型到迭代器块的生成类型(见4.1.3)必存在一个隐式转换。
yield return语句依照下面的步骤执行:
对语句中给定的表达式进行求值,并隐式转换为生成类型,然后赋给枚举器对象的Current属性。
挂起对迭代器块的执行。如果该yield return语句位于一个或多个try块中,相应的finally块暂时不会被执行。
MoveNext()方法向其调用者返回true,表示枚举器对象成功地前进到下一个值上。
对枚举器对象的MoveNext()方法的下一次调用将从上一次挂起的地方恢复对迭代器块的执行。
yield break语句依照下面的步骤执行:
如果yield break语句位于一个或多个带有finally块的try块中,控制将被转移到最里面的try块对应的finally块中。当控制流程遇到finally块的结尾(如果能够的话),控制将被转移到外一层try块对应的finally块中。这个过程持续到所有try语句对应的finally块都被执行完。
将控制返回给迭代器块的调用者。这可能从MoveNext()方法或Dispose()方法中返回。
由于一个yield break语句无条件地将控制转移到其它地方,因此一个yield break的终点将永远不可达。
4.4.1 明确赋值
对于下面形式的yield return语句:
yield return expr ;
对于一个变量v,在expr的开始处和语句的开始处有同样的明确赋值。
如果一个变量v在expr的结束处被明确赋值,则它是在语句的结尾被明确赋值的;否则,它未在语句的结尾被明确赋值。
4.5 实例
这一节将描述标准C#结构中的迭代器可能的实现。这里描述的实现是基于和Microsoft C#编译器相同的原则的,但决不是唯一可能的实现。
下面的Stack<T>类使用一个迭代器实现了它的GetEnumerator()方法。该迭代器按照从顶至底的顺序枚举了堆栈中的所有元素。
using System;
using System.Collections;
using System.Collections.Generic;
class Stack<T> : IEnumerable<T> {
T[] items;
int count;
public void Push(T item) {
if (items == null) {
items = new T[4];
}
else if (items.Length == count) {
T[] newItems = new T[count * 2];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
items[count++] = item;
}
public T Pop() {
T result = items[--count];
items[count] = T.default;
return result;
}
public IEnumerator<T> GetEnumerator() {
for(int i = count - 1; i >= 0; --i) yield items[i];
}
}
GetEnumerator()方法可以转换为编译器自动生成的枚举器类的实例,它封装了迭代器块中指定的代码,如下所示:
class Stack<T> : IEnumerable<T> {
...
public IEnumerator<T> GetEnumerator() {
return new __Enumerator1(this);
}
class __Enumerator1 : IEnumerator<T>, IEnumerator {
int __state;
T __current;
Stack<T> __this;
int i;
public __Enumerator1(Stack<T> __this) {
this.__this = __this;
}
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1: goto __state1;
case 2: goto __state2;
}
i = __this.count - 1;
__loop:
if(i < 0) goto __state2;
__current = __this.items[i];
__state = 1;
return true;
__state1:
--i;
goto __loop;
__state2:
__state = 2;
return false;
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
上面的转换中,迭代器块中的代码被转换为状态机并放在枚举器类的MoveNext()方法中。此外,局部变量i被转换为枚举器对象的域,因此在对MoveNext()方法的调用过程中它将一直存在。
下面的例子打印了整数1至10的一个简单的乘法表。例子中的FromTo()方法返回了一个用迭代器实现的可枚举对象。
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();
}
}
}
FromTo()方法可以被转换为由编译器自动生成的可枚举类的实例,它封装了迭代器块中的代码,如下所示:
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
class Test {
...
static IEnumerable<int> FromTo(int from, int to) {
return new __Enumerable1(from, to);
}
class __Enumerable1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator {
int __state;
int __current;
int __from;
int from;
int to;
int i;
public __Enumerable1(int __from, int to) {
this.__from = __from;
this.to = to;
}
public IEnumerator<int> GetEnumerator() {
__Enumerable1 result = this;
if(Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
result = new __Enumerable1(__from, to);
result.__state = 1;
}
result.from = result.__from;
return result;
}
IEnumerator IEnumerable.GetEnumerator() {
return (IEnumerator)GetEnumerator();
}
public int Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1:
if(from > to) goto case 2;
__current = from++;
__state = 1;
return true;
case 2:
__state = 2;
return false;
default:
throw new InvalidOperationException();
}
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
这个可枚举类同时实现了可枚举接口和枚举器接口,因此它既是可枚举的又是一个枚举器。当GetEnumerator()方法第一次被调用时,将返回可枚举对象本身。以后对GetEnumerator()方法的调用(如果有的话),将返回可枚举对象的一个拷贝。因此返回的每一个枚举器具有其自己的状态,一个枚举器的改变不会影响到其它的枚举器。Interlocked.CoompareExchange()方法可以用于确保线程安全。
from和to参数被转换为可枚举类的域。因为迭代器块改变了from,因此引入了一个附加的__from域来保存每个枚举器中的from的初始值。
当__state是0时,MoveNext()方法将跑出一个InvalidOperationException异常。这将保证不会发生没有首先调用GetEnumerator()方法而直接将可枚举对象用作枚举器。