相关文章推荐
讲道义的鸡蛋面  ·  Elasticsearch ...·  1 年前    · 
乖乖的砖头  ·  TDengine 与 InfluxDB ...·  1 年前    · 
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 循环来进行迭代:

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

10
11

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

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

8
9 public 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.0 System.Collections.Generic 名称空间定义了泛型、类型安全的 IEnumerable<T> IEnumerator<T> 接口:

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

8
9 public 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 中的手工实现。

1 public 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, 把树的根节点传给 ScanInOrder ScanInOrder 定义如下:

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. )。那就是说通过使用显式堆或者队列来维持仍被访问的节点列表,迭代器仍然很容易在递归数据结构例如树上使用。