图1 迭代器设计模式
此外,由于每个迭代器都保持单独的迭代状态,所以多个客户端可以执行单独的并发迭代。通过实现IEnumerable接口,诸如数组和队列这样的数据结构可以支持这种非常规的迭代。在foreach循环中生成的代码调用类的GetEnumerator方法可以简单地获得一个IEnumerator对象,然后将其用于while循环,接着,通过连续调用它的MoveNext方法来遍历集合。如果您需要显式地遍历集合,您可以直接使用IEnumerator(无须使用foreach语句)。
但是使用这种方法有一些问题。首先,如果集合包含值类型,则需要对它们进行装箱和拆箱才能获得项,因为IEnumerator.Current返回一个Object类的对象。这将导致潜在的性能降低 和托管堆上的压力增大。即使集合包含引用类型,仍然会产生从对象向下强制类型转换的不利结果。虽然大多数开发人员不熟悉这一特性,事实上在C# 1.0中,不必实现IEnumerator或IEnumerable接口就可以为每个循环实现迭代器模式。编译器将选择调用强类型化版本,以避免强制类型转换和装箱。结果是,即使在1.0版本中,也可能没有导致性能损失。
为了更好地阐明这个解决方案并使其易于实现,Microsoft .NET框架2.0在System.Collections.Generics命名空间中定义了类型安全的泛型IEnumerableItemType和IEnumeratorItemType:
public interface IEnumerable{ IEnumerator GetEnumerator();}public interface IEnumerator : IDisposable{ ItemType Current{get;} bool MoveNext();}
除了利用泛型之外,新的接口与其前身还略有差别。与IEnumerable不同,IEnumerator是从IDisposable派生而来的,并且没有Reset方法。图2中的代码显示了实现IEnumerablestring的简单city集合,而图3显示了编译器展开foreach循环的代码中如何使用该接口。图2中的实现使用了名为MyEnumerator的嵌套类,它将一个引用作为构造参数返回给要枚举的集合。MyEnumerator清楚地知道city集合(本例中的一个数组)的实现细节。此外,MyEnumerator类使用m_Current成员变量维持当前的迭代状态,此成员变量用作数组的索引。
public class CityCollection : IEnumerablestring{ string[] m_Cities = {"New York","Paris","London"}; public IEnumeratorstring GetEnumerator() { return new MyEnumerator(this); } //Nested class definition class MyEnumerator : IEnumeratorstring { CityCollection m_Collection; int m_Current; public MyEnumerator(CityCollection collection) { m_Collection = collection; m_Current = -1; } public bool MoveNext() { m_Current++; if(m_Current m_Collection.m_Cities.Length) return true; else return false; } public string Current { get { if(m_Current == -1) throw new InvalidOperationException(); return m_Collection.m_Cities[m_Current]; } } public void Dispose(){} }}
图2 实现IEnumerablestring
CityCollection cities = new CityCollection();//For this foreach loop:foreach(string city in cities){ Trace.WriteLine(city);}//The compiler generates this equivalent code:IEnumerablestring enumerable = cities;IEnumeratorstring enumerator = enumerable.GetEnumerator();using(enumerator){ while(enumerator.MoveNext()) { Trace.WriteLine(enumerator.Current); }}
图3 简单的迭代程序
第二个问题迭代器的实现也是难以解决的问题。虽然对于简单的应用实例中(如图3所示),实现是相当简单的,但是对于高级的数据结构,实现将非常复杂,例如二叉树,它需要递归遍历,并需在递归时维持迭代状态。另外,如果需要各种迭代选项,例如需要在一个链表中从头到尾和从尾到头选项,则此链表的代码就会因为使用多种迭代器实现而变得臃。这正是设计C# 2.0迭代器所要解决的问题。通过使用迭代器,可以让C#编译器生成IEnumerator的实现。C#编译器能够自动生成一个嵌套类来维持迭代状态。可以在泛型集合或特定于类型的集合中使用迭代器。开发人员需要做的只是告诉编译器在每个迭代中产生的是什么。如同手动提供迭代器一样,需要公开GetEnumerator方法,此方法是在实现IEnumerable接口或IEnumerableItemType公开的。
可以使用新的C#的yield return语句告诉编译器产生什么。例如,下面的代码显示了如何在city集合中使用C#迭代器来代替图2中的人工实现部分:
public class CityCollection : IEnumerablestring{ string[] m_Cities = {"New York","Paris","London"}; public IEnumeratorstring GetEnumerator() { for(int i = 0; im_Cities.Length; i++) yield return m_Cities[i]; }}
此外,您还可以在非泛型集合中使用C#迭代器:
public class CityCollection : IEnumerable{ string[] m_Cities = {"New York","Paris","London"}; public IEnumerator GetEnumerator() { for(int i = 0; im_Cities.Length; i++) yield return m_Cities[i]; }}
此外,还可以在如图4所示的在完全泛型(Fully Generic)集合中使用C#迭代器。当使用泛型集合和迭代器时,从声明的集合(本例中的string)中,编译器就可以检索到foreach循环内IEnumerableItemType所用的特定类型:
LinkedList list = new LinkedList();/* Some initialization of list, then */foreach(string item in list){ Trace.WriteLine(item);}
图4在普通链表中使用迭代程序
//K is the key, T is the data itemclass NodeK,T{ public K Key; public T Item; public NodeK,T NextNode;}public class LinkedListK,T : IEnumerableT{ NodeK,T m_Head; public IEnumeratorT GetEnumerator() { NodeK,T current = m_Head; while(current != null) { yield return current.Item; current = current.NextNode; } } /* More methods and members */}
这与其他任何从泛型接口派生的相似。如果想中止迭代,请使用yield break语句。例如,下面的迭代器将仅仅产生数值1、2和3:
public IEnumerator GetEnumerator(){ for(int i = 1;i 5;i++) { yield return i; if(i 2) yield break; }}
这样,集合可以很容易地公开多个迭代器,每个迭代器都用于以不同的方式遍历集合。例如,要以倒序遍历CityCollection类,在这个类中提供了IEnumerablestring类型的Reverse属性,它是
public class CityCollection{ string[] m_Cities = {"New York","Paris","London"}; public IEnumerable Reverse { get { for(int i=m_Cities.Length-1; i= 0; i--) yield return m_Cities[i]; } }}
这样就可以在foreach循环中使用Reverse属性:
CityCollection collection = new CityCollection();foreach(string city in collection.Reverse){ Trace.WriteLine(city);}
使用yield return语句是有一定限制的。包含yield return语句的方法或属性不能再包含其他return语句,否则会出现迭代中断并提示错误。不能在匿名方法中使用yield return语句,也不能将yield return语句放到带有catch块的try语句中(同样,也不能放在catch块或finally块中)。
迭代器实现
编译器通过生成的嵌套类来维护迭代状态。当在foreach循环中(或在直接的迭代代码中)首次调用迭代器时,编译器为GetEnumerator函数产生的编译生成(Compiler-Generated)代码将创建一个带有reset状态的新的迭代器对象(即嵌套类的一个实例)。在foreach每次循环调用迭代器的MoveNext方法时,它都从前一次yield return语句停止的地方开始执行。只要foreach循环执行,迭代器就会维持它的状态。然而,迭代器对象(以及它的状态)在多个foreach循环之间并不保持一致。因此,再次调用foreach是安全的,因为将生成新的迭代器对象并开始新的迭代。这就是为什么IEnumerableItemType没有定义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 : IEnumerablestring{ public virtual IEnumeratorstring GetEnumerator() { GetEnumerator$0003__IEnumeratorImpl impl; impl = new GetEnumerator$0003__IEnumeratorImpl; impl.this = this; return impl; } private class GetEnumerator$0003__IEnumeratorImpl : IEnumeratorstring { public MyCollection this; // Back reference to the collection string $_current; // state machine members go here string IEnumeratorstring.Current { get { return $_current; } } bool IEnumeratorstring.MoveNext() { //State machine management } IDisposable.Dispose() { //State machine cleanup if required } }}
图5编译器生成的迭代程序
嵌套类实现了从类成员返回的相同IEnumerable接口。编译器使用一个实例化的嵌套类型来代替类成员中的代码,将一个指向集合的引用赋给嵌套类的this成员变量,类似于图2中所示的手动实现。实际上,该嵌套类是一个实现IEnumerator接口的类。
递归迭代当在像二叉树或包含相互连通节点的图这样的数据结构上进行递归迭代时,迭代器才真正显示出了它的优势。手工实现一个递归迭代的迭代器是相当困难的,但是如果使用C#迭代器,就很容易。请考虑图6中的二叉树。本文所提供的源代码包含了此二叉树的完整实现。
class NodeT{ public NodeT LeftNode; public NodeT RightNode; public T Item;}public class BinaryTreeT{ NodeT m_Root; public void Add(params T[] items) { foreach(T item in items) Add(item); } public void Add(T item) {...} public IEnumerableT InOrder { get { return ScanInOrder(m_Root); } } IEnumerableT ScanInOrder(NodeT 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);
这棵树提供了一个IEnumerableT类型的名为InOrder的公共属性。InOrder调用私有的辅助递归函数ScanInOrder并把树的根节点传递给ScanInOrder。ScanInOrder定义如下:
IEnumerable ScanInOrder(Node root);
它返回IEnumerableT类型的迭代器的实现,此实现按顺序遍历二叉树。对于ScanInOrder需要注意的一件事情是,它通过递归遍历这个二叉树的方式,即使用foreach循环来访问从递归调用返回的IEnumerableT实现。在顺序(in-order)迭代中,每个节点都首先遍历它左边的子树,接着遍历该节点本身的值,然后遍历右边的子树。对于这种情况,需要三个yield return语句。为了遍历左边的子树,ScanInOrder在递归调用(它以参数的形式传递左边的节点)返回的IEnumerableT上使用foreach循环。一旦foreach循环返回,就已经遍历左边子树的所有节点。然后,ScanInOrder产生作为迭代的根传递给其节点的值,并在foreach循环中执行另一个递归调用,这次是在右边的子树上。
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)通过使用属性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)垃圾回收。也就是说,通过使用栈或队列维护一列将要被检查的节点,迭代器仍然能够方便地遍历递归数据结构(例如树)。
局部类型
C# 1.1中要求将类的全部代码放在一个文件中。而在C# 2.0允许将类或结构的定义和实现分开放在多个文件中。通过使用新的partial关键字来标注分割,可以将类的一部分放在一个文件中,而将另一个部分放在一个不同的文件中。例如,可以将下面的代码放到文件MyClass1.cs中:
public partial class MyClass{ public void Method1() {...}}
在文件MyClass2.cs中,可以插入下面的代码:
public partial class MyClass{ public void Method2() {...} public int Number;}
实际上,可以将任一特定的类分割成任意多的部分。局部类型支持可以用于类、结构和接口,但是不能包含局部枚举定义。局部类型是一个非常有用的功能。有时,需要修改机器生成的文件,例如Web服务客户端包装类。然而,当重新生成此包装类时,对该文件的修改将会被丢弃。通过使用局部类,可以将这些改变分开放在单独的文件中。在ASP.NET中可以将局部类用于code-beside编辑(从code-behind演变而来),单独存储页面中机器生成的部分,而在Windows窗体中使用局部类来存储InitializeComponent方法的可视化设计器输出以及成员控件。通过使用局部类型,两个或者更多的开发人员可以工作在同一个类型上,同时都可以从源代码控制中签出其文件而不互相影响。
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)但是,如果多个不同的部分对同一个类做出了相互矛盾的定义会出现什么样的后果?答案很简单。一个类(或一个结构)可能具有两个不同的方面或性质:累积性的(accumulative)和非累积性的(non-accumulative)。累积性的方面是指类可以选择添加它的各个部分,比如接口派生、属性、索引器、方法和成员变量。例如,下面的代码显示了一个部分是如何添加接口派生和实现的:
public partial class MyClass{}public partial class MyClass : IMyInterface{ public void Method1() {...} public void Method2() {...}}
非累积性的方面是指一个类型的所有部分都必须一致。无论这个类型是一个类还是一个结构,类型可见性(公共或内部)和基类都是非累积性的方面。例如,下面的代码不能编译,因为并非MyClass的所有部分都出现在基类中:
public class MyBase{}public class SomeOtherClass{}public partial class MyClass : MyBase{}public partial class MyClass : MyBase{}//Does not compilepublic partial class MyClass : SomeOtherClass{}
除了所有的部分都必须定义相同的非累积性部分以外,只有一个部分能够重写虚方法或抽象方法,并且只有一个部分能够实现接口成员。
C# 2.0是这样来支持局部类型的:当编译器构建程序集时,它将来自多个文件的同一类型的各个部分组合起来,并用中间语言(Microsoft intermediate language, MSIL)将这些部分编译成单一类型。生成的中间语言中不含有哪一部分来自哪个文件的记录。正如在C# 1.1中一样。另外值得注意的是,局部类型不能跨越程序集,并且通过忽略其定义中的partial限定符,一个类型可以拒绝包含其他部分。 因为编译器所做的只是将各个部分累积,所以一个单独的文件可以包含多个部分,甚至是包含同一类型的多个部分,尽管这样做的意义值得怀疑。
在C#中,开发人员通常根据文件所包含的类来为文件命名,这样可以避免将多个类放在同一个文件中。在使用局部类型时,建议在文件名中指示此文件包含哪个类型的哪些部分(例如MyClassP1.cs、MyClassP2.cs),或者采用其他一致的方式从类的名称上指示源文件的内容。例如,Windows窗体设计人员将用于该窗体的局部类的一部分存放在Form1.cs中,并将此文件命名为Form1.Designer.cs。 局部类的另一个不利之处是,当开始接触一个不熟悉的代码时,所维护的类的各个部分可能遍布在整个项目的文件中。在这种情况下,可以使用Visual Studio Class View,因为它可以将一个类型的所有部分积累起来展示给您,并允许通过单击它的成员来导航各个不同的部分。导航栏也提供了这个功能。