梦想与现实的落差,就是我们离成功的距离!!

博客园 首页 新随笔 联系 订阅 管理
  http://msdn.microsoft.com/msdnmag/issues/06/00/C20/default.aspx#S1



             

使用匿名方法迭代器局部类书写优雅的C#代码


        就如在Visual C#® 2005中那样,C#语言爱好者将会发现Visual Studio® 2005也给C#带来了很多激动人心的新特性,例如泛型、迭代器、局部类,还有匿名方法。虽然泛型是讨论得最多的特性,特别是在那些熟悉模板的C++开发人员中,其它新特性对于你的Microsoft® .NET开发兵工厂来说也是非常重要的。相比先前的C#版本这些重要的特性和语言的附增物将会全面地提升你的生产力,让你更快的写出更加清晰的代码。

迭代器Iterators

C#1.0中,你能在如数组和集合等数据结构上使用foreach循环来进行迭代:

 1string[] cities = {"New York","Paris","London"};
 2
 3foreach(string city in cities)
 4
 5{
 6
 7   Console.WriteLine(city);
 8
 9}

10
11

事实上你可以你可以在foreach循环中使用任何自定义的数据集合,只要那集合类型实现了一个GetEnumerator方法,该方法返回一个IEnumerator接口。通常通过实现IEnumerable接口来达到该目的:

 1public interface IEnumerable 
 2
 3{
 4
 5   IEnumerator GetEnumerator();
 6
 7}

 8
 9public interface IEnumerator 
10
11{
12
13   object Current{get;}
14
15   bool MoveNext();
16
17   void Reset();
18
19}

20
21

通过实现IEnumerable接口而用来在一个集合之上进行迭代的类,通常被提供作为集合类型的一个嵌套类而被迭代。这个迭代器类型维持迭代的状态。通常把嵌套类作为一个枚举器要更加好,因为它可以访问包含它的类的所有私有成员。当然,这是迭代器设计模式,它对将要进行迭代的客户端屏蔽了底层数据结构的实际实现细节,使得客户端对多种数据结构能用同样的迭代逻辑,就如图1中所示。


图1 迭代器设计模式
 

另外,因为每个迭代器维持各自的迭代状态,所以多客户能并发执行各自的迭代。像数组(Array)和队列(Quene)这样的数据结构通过实现IEnumerable接口来支持箱外迭代,在foreach循环中生成的代码仅仅包含了一个通过调用类的GetEnumerator方法得到的IEnumerator对象,在一个while循环中通过不断调用它的MoveNext方法和Current属性对集合进行迭代。如果你需要显式地对集合进行迭代,那么你能直接使用IEnumerator(没有foreach语句)。

但是用这种方法有一些问题。首先,如果集合包含值类型,获取其中的元素就需要对他们进行装箱和拆箱工作,因为IEnumerator.Current返回的是一个对象。这样的结果就是潜在的性能降级,还有在被管理的堆上压力增大。即使集合包含的是引用类型,你仍然会导致把对象向下转型的性能损失。然而,许多开发人员都不熟悉,在C#1.0中,你可以不通过实现IEnumerator或者IEnumerable接口就可以真正地实现适合于foreach循环的迭代器模式。编译器将调用强类型版本,避免转型和装箱,结果就是,即使在C#1.0版本中,也有可能不引起性能损失。

为了更好地阐明这种解决方案以及更加容易实现它,Microsoft .NET Framework 2.0System.Collections.Generic名称空间定义了泛型、类型安全的IEnumerable<T> IEnumerator<T>接口:

 

 1public interface IEnumerable<T> : IEnumerable
 2
 3{   
 4
 5   IEnumerator<T> GetEnumerator();
 6
 7}
 
 8
 9public interface IEnumerator<T> : IEnumerator,IDisposable
10
11{
12
13   T Current{get;}
14
15}
       
16
17

因为泛型接口是派生自非泛型的,所以任何预期使用老接口的遗留客户端,它也能和一个支持新接口的新集合一起工作,这产生的副作用就是在你的集合代码上你必须使用显式接口实现,因为你不能仅仅基于返回类型而重载方法。

2中的代码展示了一个简单的实现了接口IEnumerable<string>的城市集合,还有在3显示了当跨越foreach循环的代码时编译器怎样使用那个接口。在2中,该实现使用了一个叫做MyEnumerator的嵌套类,它接受一个被用来枚举的集合的引用作为构造函数的参数。MyEnumerator类清楚地知道城市集合的实现细节,在该范例中是一个数组。这个MyEnumerator类在m_Current成员变量中维持当前的迭代状态,它被用作数组中的索引。也注意到在2中非泛型方法IEnumerable.GetEnumerator IEnumerator.Current属性把它们的实现委托给新接口的泛型方法。

第二个更加困难的问题是实现迭代。虽然对于简单情况,实现迭代是简单而直接的(就如 2所示)。但是对更加高级的数据结构,例如二进制树,那就是挑战了,因为二进制树需要递归往复并在整个递归中维持迭代状态。此外,如果你要求多种迭代选项,例如在链表上的从头至尾和从尾至头迭代,这种适应于链表的代码将会变得臃肿,因为它需要实现多种迭代器。这的确就是C#2.0迭代器被设计来解决的问题。使用迭代器,你能让C#编译器为你生成IEnumerator 或者 IEnumerator<T>的实现。C#编译器能自动生成一个嵌套类来维持迭代状态。你能在一个泛型集合或者一特定类型集合上使用迭代器。所有你需要做的就是告诉编译器在每次迭代中输出什么。与手工提供一个迭代器一样,你需要暴露一个GetEnumerator方法,典型地你通过实现IEnumerable IEnumerable<T>来完成。

使用新的C#yield返回语句你告诉编译器输出什么。例如,这儿就是在城市集合中怎样使用C#迭代器代替2中的手工实现。

 1public class CityCollection : IEnumerable<string>
 2
 3{
 4
 5   string[] m_Cities = {"New York","Paris","London"};
 6
 7   IEnumerator<string> IEnumerable<string>.GetEnumerator()
 8
 9   {
10
11      for(int i = 0; i<m_Cities.Length; i++)
12
13         yield return m_Cities[i];
14
15   }

16
17   IEnumerator IEnumerable.GetEnumerator()
18
19   {
20
21      return  ((IEnumerable<string>)this)GetEnumerator();
22
23   }

24
25}

26
27

你也能在非泛型集合上使用C#迭代器:

public class CityCollection : IEnumerable

{

   
string[] m_Cities = {"New York","Paris","London"};

   
public IEnumerator GetEnumerator()

   
{

      
for(int i = 0; i<m_Cities.Length; i++)

         yield 
return m_Cities[i];

   }


}


另外,你能在纯泛型的集合上使用C#迭代器,如4中所显示的。当使用一个泛型集合和迭代器时,编译器从声明集合时使用到的类型就知道,在foreach循环中对于IEnumerable<T>接口用到的确切的类型是什么,在本范例中是string

LinkedList<int,string> list = new LinkedList<int,string>();

// Some initialization of list, then  

foreach(string item in list)

{   

   Trace.WriteLine(item);

}

这与任何派生自一个泛型接口的派生类都是类似的。

如果你因某些原因想中途停止迭代,使用yield语句中断语句,例如,下面的迭代器仅产生1、2和3这三个值:

public IEnumerator<int> GetEnumerator()

{

   
for(int i = 1; i <5; i++)

   {

      yield 
return i;

      
if(i > 2)

         yield 
break;

   }

}

你的集合能轻松地暴露多迭代器,每个都可以被用来以不同地方式遍历集合。例如,可以提供一个类型为IEnumerable<string>的叫做Reverse的属性,以便能以倒转的顺序遍历CityCollection类。

public class CityCollection 

{   

   
string[] m_Cities = {"New York","Paris","London"};

   
public IEnumerable<string> Reverse

   {

      
get

      {

         
for(int i=m_Cities.Length-1; i>= 0; i--)

            yield 
return m_Cities[i];         

      }

   }

}  

然后在foreach循环中使用Reserve属性: 

CityCollection collection 
= new CityCollection();

foreach(string city in collection.Reverse)

{   

   Trace.WriteLine(city);

}

什么地方使用以及怎样使用yield返回语句是有一些限制的。包含yield返回语句的方法或属性不能同时包含一个普通返回语句,因为那样不能正确地中断迭代。你不能在一个匿名方法中使用yield返回语句,同样在try语句中有它(也不能在catch或者finally块中).

 

迭代器实现

编译器生成嵌套类维持迭代状态。当迭代器第一次在foreach循环中(或者在直接的迭代代码中)被调用时,编译器为GetEnumerator生成的代码创建一个处于重置状态的迭代器对象(一个嵌套类的实例),每次foreach循环和调用迭代器的MoveNext方法时,它在先前的yield返回语句释放的地方开始执行。只要foreach循环在执行,迭代器就维持它的状态,然而,迭代器对象(还有它的状)不能持续跨越所有foreach循环,因此,再次调用foreach循环是安全的,因为你得到的是一个新的迭代器对象并开始一个新的迭代。

但是嵌套迭代器该如何实现以及如何管理它的状态呢?编译器把一个标准方法转化成一个被设计能供多次调用且使用一个简单状态机可以在先前的yield返回语句后恢复执行的方法。编译器甚至能足够精确地能以yield返回语句出现的次序来连接它们:

public class CityCollection : IEnumerable<string>

{

   IEnumerator
<string> IEnumerable<string>.GetEnumerator()

   {

      yield 
return "New York";

      yield 
return "Paris";

      yield 
return "London";

   }

   IEnumerator IEnumerable.GetEnumerator() {}

}

让我们在下面几行代码中的看看类的IEnumerable<string>.GetEnumerator方法:

public class MyCollection : IEnumerable<string>

{

   IEnumerator
<string> IEnumerable<string>.GetEnumerator()

   {

      
//Some iteration code that uses yield return 

   }

   IEnumerator IEnumerable.GetEnumerator() {}

}

当编译器遇到像这样一个包含yield返回语句的类成员,它导入一个嵌套类的定义,该类的名称是”GetEnumerator”再加上一个惟一由字母和数组组成的字符串。就如5所示的C#伪代码中那样。(你应该记住编译器生成的类和字段的名称是不固定的,在将来的版本中会改变,你不能试图用反射来得到那些实现细节和期望每次都得到一致的结果。)

嵌套类实现从类成员返回的相同的IEnumerable IEnumerable<T>接口。编译器用嵌套类实例化来替换类成员中的代码,把返回给集合的一个引用分派给嵌套类,类似在2中那种手工实现。嵌套类是真正提供IEnumerator IEnumerator<T>接口实现的类。

 

递归迭代

迭代器真正闪光的地方是当它在那种如二进制树或者任何复杂的相互连接的节点图表上进行迭代递归时。手工实现一个能递归迭代的迭代器是非常困难的,然而使用C#迭代器来处理就非常容易了。考虑如Figure 6的二进制树。树的全部实现代码是这篇文章的可用的源代码的一部分。

二进制树在节点中存储元素。每个节点持有一个泛型类型T的值,称之为元素。每个节点都有一个指向左节点和右节点的引用。比该元素的值小的存储在左边的子树中,同理,比该元素值大的存储在右边的子节点。该树也为了加入一个类型为T的无限制的数组提供一个Add方法,使用params限定词:

public void Add(params T[] items);

树提供了类型为IEnumerable<T>类型的名叫”InOrder”的公共属性。InOrder调用递归的私有辅助方法ScanInOrder,把树的根节点传给ScanInOrderScanInOrder定义如下:

IEnumerable<T> ScanInOrder(Node<T> root);

它返回类型为IEnumerable<T>的一个迭代器的实现,它按顺序遍历二进制树。关于ScanInOrder有意思的是它在二进制树上使用递归来进行迭代的方法,它使用foreach循环来访问从一次递归调用中返回的IEnumerable<T> 使用中序迭代,就是每个节点先在它左侧的子树上迭代,然后就自己,最后是在它右侧子树上迭代。为此,你需要有三个yield返回语句。在左侧子树上迭代时,ScanInOrder在由一次递归调用返回的IEnumerable<T>上进行foreach循环,它把左侧子节点作为参数传人。一旦foreach循环返回了,所有左侧子树的节点已经被迭代和输出了。ScanInOrder然后输出本节点的值,把它作为迭代的根传入,在foreach循环中执行另一次递归,这次是在右侧的子树了。

InOrder属性允许写出如下foreach循环,对整棵树进行迭代:

BinaryTree<int> tree = new BinaryTree<int>();

tree.Add(4,6,2,7,5,3,1);

foreach(int number in tree.InOrder)

{

   Trace.WriteLine(number);

}

// Traces 1,2,3,4,5,6,7

通过增加一些附加属性,你能以类似的方式实现前序和后序迭代。

虽然这种使用递归迭代器的能力是一种非常明显的强有力的特征,但是应该小心使用它,因为可能有严重的潜在性能问题。每次调用ScanInoder时都需要一个由编译器生成的迭代器的实例,因此在一棵深度树进行递归迭代时能导致在后台生成了大量的对象。在一棵平衡二进制树中,大约有n个迭代器实例,n是该树的节点数。在任一给定时刻,大约有log(n)的对象是活动的。在一棵适度规模的树中,那些大量的对象将使得它被通过第0代垃圾收集(这句真的不知道怎么翻译!请大家指点。后面就是原句!!)In a decently sized tree, a large number of those objects will make it past a Generation 0 garbage collection.)。那就是说通过使用显式堆或者队列来维持仍被访问的节点列表,迭代器仍然很容易在递归数据结构例如树上使用。

posted on 2006-02-10 14:02  叶漂  阅读(2662)  评论(8编辑  收藏  举报