C#中的迭代器和yield
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
在.NET中通过使用接口IEnumerator
和IEnumerable
及其泛型等价物。来封装 迭代器模式 。所谓 迭代器模式 即是:允许你访问一个数据项序列,能依次遍历该序列中的每一项元素,而无需关心元素在序列内部的组织形式。
1.非泛型版本的IEnumerator和IEnumerable接口
首先看IEnumerator接口的定义,代码如下:
public interface IEnumerator
{
/*
获取集合中位于枚举数当前位置的元素。在下列任一情况下,不定义Current:
1 枚举器位于集合中第一个元素之前,紧跟在创建枚举器之后。 在读取Current的值之前,
必须调用MoveNext,以将枚举器前进到集合的第一个元素。
2 对MoveNext的最后一次调用返回了 false,指示集合的末尾。
3 由于对集合所做的更改(如添加、修改或删除元素),枚举器无效。
在调用 Current 之前,MoveNext返回相同的对象。 MoveNext将Current设置为下一个元素。
*/
object Current { get; }
/*
将枚举数推进到集合的下一个元素。
如果枚举数已成功地推进到下一个元素,则为true;
如果枚举数传递到集合的末尾,则为false。
在创建枚举器之后,或在调用Reset方法之后,枚举数将定位到集合的第一个元素之前,
对MoveNext方法的第一次调用将枚举器移动到集合的第一个元素上。
如果MoveNext越过集合的末尾,则枚举器将定位到集合中的最后一个元素之后,MoveNext返回false。
当枚举器位于此位置时,对MoveNext的后续调用也将返回false,直到调用Reset。
如果对集合所做的更改(如添加、修改或删除元素),则MoveNext的行为是不确定的。
*/
bool MoveNext();
/*
将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。
*/
void Reset();
}
顾名思义,IEnumerator
就是表征一个“ 迭代器 ”(又可称为 枚举器 )应有的功能接口,该接口所函数的方法如上述代码注释中所描述,接下来看IEumerable
接口,代码如下:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerable
用来表征“一个数据集合是 可迭代 的( 可枚举 的)”,即是说该数据集合中的每个元素都是可以被一一迭代的。IEnumerable
接口也很简单,只有一个方法,就是返回 能访问到该数据集合每个元素的迭代器 。
通过上述两个interface可以看到,如果一个数据集合,要定义其为可枚举的,则它需要继承实现IEnumerable
接口。并且同时得定义实现一个迭代器类,这个类要继承实现IEnumerator
接口。下面举示例代码说明:
class IteratableDataStruct : IEnumerable
{
object[] Values;
// 数据集合中,作为第一个元素的位置索引值
public int StartingPoint
{
get;
set;
}
public IteratableDataStruct(object[] values, int startingPoint)
{
this.values = values;
this.StartingPoint = startingPoint;
}
public IEnumerator GetEnumerator()
{
return new DataStructIterator(this);
}
// 迭代器实现
class DataStructIterator : IEnumerator
{
// 重要数据成员,来获取当前值,下一个值,是否结束。在构造函数中赋值。
IteratableDataStruct m_DataStruct;
// 迭代器游标
int m_IteratorPosition;
internal public IterationSampleIterator(IteratableDataStruct ds)
{
m_DataStruct = ds;
m_IteratorPosition = -1; // 迭代器游标按照标准,初始化时,游标器在表头第一项之前
}
// 重载实现interface IEnumerator的同名方法
public bool MoveNext()
{
// 如果没有超出数组的长度,就允许迭代器游标位置增加
if (m_IteratorPosition != m_DataStruct.values.Length)
{
m_IteratorPosition++;
}
// 按MoveNext方法的定义,迭代器游标超出
return m_IteratorPosition < parent.values.Length;
}
// 重载实现interface IEnumerator的同名属性
public object Current
{
get
{
// 如果当前迭代器游标位置不合法就排除异常
if (m_IteratorPosition == -1 || m_IteratorPosition == parent.values.Length)
{
throw new InvalidOperationException();
}
int index = (m_IteratorPosition + m_DataStruct.StartingPoint);
index = index % m_DataStruct.values.Length;
return m_DataStruct.values[index];
}
}
// 重载实现interface IEnumerator的同名方法
public void Reset()
{
// 迭代器游标按照标准,重置时,游标器在表头第一项之前
m_IteratorPosition = -1;
}
} // class DataStructIterator
} // class IteratableDataStruct
上面代码段就是对迭代器和可迭代数据结构的定义。下面的伪代码段则演示了如何迭代遍历数据结构内的每一个元素:
var ds = new IteratableDataStruct(...); //
var iterator = sample.GetEnumerator(); // 拿到该data struct对应的迭代器
while(iterator.MoveNext()) // 调用迭代器的MoveNext方法,依次获取每一个元素
{
item = iterator.Current;
.....
}
如果不想显式地使用数据结构的GetEnumerator
方法,以及迭代器的MoveNext
方法和Current
属性, 可以使用语法关键字foreach
,把上面的代码 合三为一 ,如下:
// 如果不想显式地使用数据结构的GetEnumerator方法,以及迭代器的MoveNext方法,
// 可以使用语法关键字foreach,在一个循环内遍历每一个元素
foreach(var item in sample)
{
.....
}
上面的定义和使用迭代器的方法,是C#1.0时代就有的。代码非常直观易懂,但就是略显繁琐。比如,要操作的数据其实都在继承实现了IEnumerable
接口的IteratableDataStruct
类中,为了操作在此类中的数据,不得不再定义实现另一个辅助工具性质的类。所以从C#2.0时代开始,引入了yield return
关键字,使用此关键字,可以省略了作为工具性质的、显式地对IEnumertor
接口进行继承实现的迭代器类,而只需要在IEnumerable
接口中的GetEnumerator()
方法中,定义如何去迭代集合中的每一个元素的方法就好。所以上述的代码使用yield return
关键字可以改写为:
class IteratableDataStruct : IEnumerable
{
object[] Values;
// 数据集合中,作为第一个元素的位置索引值
public int StartingPoint
{
get;
set;
}
public IteratableDataStruct(object[] values, int startingPoint)
{
this.Values = values;
this.StartingPoint = startingPoint;
}
public IEnumerator GetEnumerator()
{
for (int index = 0; index <Values.Length; index++)
{
yield return Values[(index + StartingPoint) % Values.Length];
}
}
}
如果刚开始看这个用yield return
编写的方法时,可能会觉得 有点儿别扭———明明要返回的是一个实现了IEumerator
接口的类的实例对象。怎么现在看起来是“返回”了数据集合中每一个要枚举的元素。这里的for循环,看起来倒是有点儿像在一个循环里面调用MoveNext
方法去访问每一个元素……
实质上,yield return关键字就类似于一个语法糖,相当于是把DataStructIterator
类的MoveNext
方法,Reset
方法和Current
属性,在IteratableDataStruct.GetEnumerator()
方法里头进行声明和定义。而实质上,编译器在碰到使用了yield return
语句中的代码后,确实会“帮忙生成”类似于前面的DataStructIterator
类的工具类。具体实现的原理可以参阅这篇文章:c# yield关键字的用法。
关于yield
- 可以在方法,属性和索引器中使用
yield
来实现迭代器。 使用yield
,函数返回类型必须是IEnumberable<T>
、IEnumberable
、IEnumberator<T>
和IEnumberator
中的任意一个。函数参数不能是ref
或out
。 try
和catch
语句里面不能出现yield return
。- 不能在匿名方法中用迭代器代码块,也就是
yield
。