扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
作者:佚名 来源:msdn 2007年11月13日
关键字:
编译器通过生成的嵌套类来维护迭代状态。当在foreach循环中(或在直接的迭代代码中)首次调用迭代器时,编译器为GetEnumerator函数产生的编译生成(Compiler-Generated)代码将创建一个带有reset状态的新的迭代器对象(即嵌套类的一个实例)。在foreach每次循环调用迭代器的MoveNext方法时,它都从前一次yield return语句停止的地方开始执行。只要foreach循环执行,迭代器就会维持它的状态。然而,迭代器对象(以及它的状态)在多个foreach循环之间并不保持一致。因此,再次调用foreach是安全的,因为将生成新的迭代器对象并开始新的迭代。这就是为什么IEnumerable<ItemType>没有定义Reset方法的原因。
但是嵌套迭代器类是如何实现的呢?并且如何管理它的状态呢?编译器将一个标准方法转换成一个可以被多次调用的方法,此方法使用一个简单的状态机在前一个yield return语句之后恢复执行。开发人员需要做的只是使用yield return语句指示编译器产生什么以及何时产生。编译器具有足够的智能,它甚至能够将多个yield return语句按照它们出现的顺序连接起来:
public class CityCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return "New York";
yield return "Paris";
yield return "London";
}
}
让我们看一看在下面几行代码中显示的该类的GetEnumerator方法:
public class MyCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
//Some iteration code that uses yield return
}
}
当编译器遇到这种带有yield return语句的类成员时,它会插入一个名为GetEnumerator$<random unique number>__IEnumeratorImpl的嵌套类的定义,如图5中C#伪代码所示。(记住,本文所讨论的所有特征,包括编译器生成的类和字段的名称是会改变的,在某些情况下甚至会发生彻底的变化。您不应该试图使用反射来获得这些实现细节并期望得到一致的结果。)
public class MyCollection : IEnumerable<string>
{
public virtual IEnumerator<string> GetEnumerator()
{
GetEnumerator$0003__IEnumeratorImpl impl;
impl = new GetEnumerator$0003__IEnumeratorImpl;
impl.<this> = this;
return impl;
}
private class GetEnumerator$0003__IEnumeratorImpl :
IEnumerator<string>
{
public MyCollection <this>; // Back reference to the collection
string $_current;
// state machine members go here
string IEnumerator<string>.Current
{
get
{
return $_current;
}
}
bool IEnumerator<string>.MoveNext()
{
//State machine management
}
IDisposable.Dispose()
{
//State machine cleanup if required
}
}
}
图5编译器生成的迭代程序
嵌套类实现了从类成员返回的相同IEnumerable接口。编译器使用一个实例化的嵌套类型来代替类成员中的代码,将一个指向集合的引用赋给嵌套类的this成员变量,类似于图2中所示的手动实现。实际上,该嵌套类是一个实现IEnumerator接口的类。
当在像二叉树或包含相互连通节点的图这样的数据结构上进行递归迭代时,迭代器才真正显示出了它的优势。手工实现一个递归迭代的迭代器是相当困难的,但是如果使用C#迭代器,就很容易。请考虑图6中的二叉树。本文所提供的源代码包含了此二叉树的完整实现。
class Node<T>
{
public Node<T> LeftNode;
public Node<T> RightNode;
public T Item;
}
public class BinaryTree<T>
{
Node<T> m_Root;
public void Add(params T[] items)
{
foreach(T item in items)
Add(item);
}
public void Add(T item)
{...}
public IEnumerable<T> InOrder
{
get
{
return ScanInOrder(m_Root);
}
}
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;
}
}
}
}
图6实现递归迭代
这个二叉树在节点中存储了一些项。每个节点均拥有一个类型T(名为Item)的值。每个节点均含有指向左边节点的引用和指向右边节点的引用。比Item小的值存储在左边的子树中,比Item大的值存储在右边的子树中。这个树还提供了Add方法,通过使用参数限定符添加一组的T类型的值:
public void Add(params T[] items);
这棵树提供了一个IEnumerable<T>类型的名为InOrder的公共属性。InOrder调用私有的辅助递归函数ScanInOrder并把树的根节点传递给ScanInOrder。ScanInOrder定义如下:
IEnumerable ScanInOrder(Node root);
它返回IEnumerable<T>类型的迭代器的实现,此实现按顺序遍历二叉树。对于ScanInOrder需要注意的一件事情是,它通过递归遍历这个二叉树的方式,即使用foreach循环来访问从递归调用返回的IEnumerable<T>实现。在顺序(in-order)迭代中,每个节点都首先遍历它左边的子树,接着遍历该节点本身的值,然后遍历右边的子树。对于这种情况,需要三个yield return语句。为了遍历左边的子树,ScanInOrder在递归调用(它以参数的形式传递左边的节点)返回的IEnumerable<T>上使用foreach循环。一旦foreach循环返回,就已经遍历左边子树的所有节点。然后,ScanInOrder产生作为迭代的根传递给其节点的值,并在foreach循环中执行另一个递归调用,这次是在右边的子树上。
通过使用属性InOrder,可以编写下面的foreach循环来遍历整个树:
BinaryTree tree = new BinaryTree();
tree.Add(4,6,2,7,5,3,1);
foreach(int num in tree.InOrder)
{
Trace.WriteLine(num);
}
// Traces 1,2,3,4,5,6,7
可以通过添加其他的属性用相似的方式实现前序(pre-order)和后序(post-order)迭代。虽然以递归方式使用迭代器的能力显然是一个强大的功能,但是在使用时应该保持谨慎,因为可能会出现严重的性能问题。每次调用ScanInOrder都需要实例化编译器生成的迭代器,因此,递归遍历一个很深的树可能会导致在幕后生成大量的对象。在对称二叉树中,大约有n个迭代器实例,其中n为树中节点的数目。在任一特定的时刻,这些对象中大约有log(n)个是活的。在具有适当大小的树中,许多这样的对象会使树通过0代(Generation 0)垃圾回收。也就是说,通过使用栈或队列维护一列将要被检查的节点,迭代器仍然能够方便地遍历递归数据结构(例如树)。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者