NET泛型编程已经离我们不远了,在微软最近随SQL Server Yukon Beta1发行的.NET Framework 1.2中就已经有了泛型的影子。虽然现在它还是问题多多,但是相信随着新版.NET Framework的正式发行,这些问题会得到解决。因此我们也该为.NET泛型编程做些准备了。
.NET系统是一个单根继承系统,所有的类型都派生自Object。我以前一直认为在单根继承系统中用不着泛型。既然所有的东西都可以作为Object传递,又何必使用泛型呢?只是增加复杂度而已,除了看起来高深一点,似乎没有别的什么好处了。但是,当两个最著名的单根系统,Java和.NET,都势不可挡地要加入泛型编程时,我不免要重新审视这个问题——为什么一定要泛型编程?
归纳起来,泛型比非泛型具有下面两个优点:
1、 更加安全
在非泛型编程中,虽然所有的东西都可以作为Object传递,但是在传递的过程中免不了要进行类型转换。而类型转换在运行时是不安全的。使用泛型编程将可以减少不必要的类型转换,从而提高安全性。
2、 效率更高
在非泛型编程中,将简单类型作为Object传递时会引起装箱和拆箱的操作,这两个过程都是具有很大开销的。使用泛型编程就不必进行装箱和拆箱操作了。
.NET泛型具有很好的二进制重用性。这一点得益于.NET将泛型内建在CLR之中。C++泛型和评估中Java泛型所依靠的是它们各自的编译器所提供的特性,编译器在编译泛型代码时将确切的类型展开,这就难免会出现代码膨胀的问题。而.NET的泛型代码是在运行时由JIT即时编译的,这样CLR就可以为不同类型重用大部分的即时编译代码了。
新C#将会支持泛型(Generics)、迭代器(Iterator)等泛型编程的特性。泛型可以使程序员更多的关注不同类型的共通算法的设计,由此大大提高开发程序的速度。
泛型的使用原则和实现原理是将操作类型参数化,用泛化的参数来传递参数类型的信息。这样,象List等通用算法就可以简单的写出一个泛型版本,在编译器编译的时候根据泛化的类型不同而生成不同的类,不会占用运行期时间,这样即节省了开发时间,也节省了运行时间。
说了半天我们来看看一个泛型的例子,大家对它就会有一个真切的了解了。
public class Stack
{
object[] m_Items;
public void Push(object item) {...}
public object Pop() {...}
}
这是一个没有使用泛型的堆栈类代码。下面看看我们一般怎么使用它。
Stack stack = new Stack();
stack.Push("1");
string number = (string)stack.Pop();
Stack stack = new Stack(); stack.Push(1);
string number = (string)stack.Pop();
这段代码在使用的过程中,简单类型将会被装箱,这样才可能将它推入堆栈。而在弹出的过程中,简单类型的数据又需要拆箱过程来取回值。这样消耗了大量的计算时间。拆箱后的数据类型是一个强制转换的过程,可能会造成数据类型的错误,而留下隐患。
public class Stack
{
T[] m_Items;
public void Push(T item) {...}
public T Pop() {...}
}
Stack stack = new Stack(); stack.Push(1);
stack.Push(2);
int number = stack.Pop();
这就是泛型的最一般的例子。当一个泛型类,被泛化的时候,如上面的Stack stack …出现的时候,编译器就会生成一个类。这个类的结构和Stack所描述的是一样的,特殊的是它使用int来代替T来生成一个类我们假定它叫做intStack,所有的stack对象的操作,都会直接按照它是一个intStack类型来处理。这样如果还有其他的泛化形式,也会相应的生成对应的类。如果泛化的类型是一个引用类型,那么编译器会把它转化成Object的Stack类,也就是说不会为每一个引用类型的泛型做一次泛化。
这样虽然最后生成的程序体积会有所增加,但是大大减少了编程时间和运行时间,也提高了程序的类型安全性。使用Stack等表示容器的泛型在编程时必然会碰到对容器中单个数据的操作问题。解决之道就是下面要介绍的迭代器。
C#中的foreach语句用于迭代一个可枚举(enumerable)的集合中的元素。为了实现可枚举,一个集合必须要有一个无参的、返回枚举器(enumerator)的GetEnumerator方法。通常,枚举器是很难实现的,因此简化枚举器的任务意义重大。
迭代器(iterator)是一块可以产生(yields)值的有序序列的语句块。迭代器通过出现的一个或多个yield语句来区别于一般的语句块:
• yield return语句产生本次迭代的下一个值。
• yield break语句指出本次迭代完成。
只要一个函数成员的返回值是一个枚举器接口(enumerator interfaces)或一个可枚举接口(enumerable interfaces),我们就可以使用迭代器:
• 所谓枚举器借口是指System.Collections.IEnumerator和从System.Collections.Generic.IEnumerator构造的类型。
• 所谓可枚举接口是指System.Collections.IEnumerable和从System.Collections.Generic.IEnumerable构造的类型。
理解迭代器并不是一种成员,而是实现一个功能成员是很重要的。一个通过迭代器实现的成员可以用一个或使用或不使用迭代器的成员覆盖或重写。
下面的Stack类使用迭代器实现了它的GetEnumerator方法。其中的迭代器按照从顶端到底端的顺序枚举了栈中的元素。
using System.Collections.Generic;
public class Stack: IEnumerable
{
T[] items;
int count;
public void Push(T data) {...}
public T Pop() {...}
public IEnumerator GetEnumerator() {
for (int i = count – 1; i >= 0; --i) {
yield return items[i];
}
}
}
GetEnumerator方法的出现使得Stack成为一个可枚举类型,这允许Stack的实例使用foreach语句。下面的例子将值0至9压入一个整数堆栈,然后使用foreach循环按照从顶端到底端的顺序显示每一个值。
using System;
class Test
{
static void Main() {
Stack stack = new Stack();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack) Console.Write("{0} ", i);
Console.WriteLine();
}
}
这个例子的输出为:
9 8 7 6 5 4 3 2 1 0
语句隐式地调用了集合的无参的GetEnumerator方法来得到一个枚举器。一个集合类中只能定义一个这样的无参的GetEnumerator方法,不过通常可以通过很多途径来实现枚举,包括使用参数来控制枚举。在这些情况下,一个集合可以使用迭代器来实现能够返回可枚举接口的属性和方法。例如,Stack可以引入两个新的属性——IEnumerable类型的TopToBottom和BottomToTop:
using System.Collections.Generic;
public class Stack: IEnumerable
{
T[] items;
int count;
public void Push(T data) {...}
public T Pop() {...}
public IEnumerator GetEnumerator() {
for (int i = count – 1; i >= 0; --i) {
yield return items[i];
}
}
public IEnumerable TopToBottom {
get {
return this;
}
}
public IEnumerable BottomToTop {
get {
for (int i = 0; i < count; i++) {
yield return items[i];
}
}
}
}
TopToBottom属性的get访问器只返回this,因为堆栈本身就是一个可枚举类型。BottomToTop属性使用C#迭代器返回了一个可枚举接口。下面的例子显示了如何使用这两个属性来以任意顺序枚举栈中的元素:
using System;
class Test
{
static void Main() {
Stack stack = new Stack();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack.TopToBottom) Console.Write("{0} ", i);
Console.WriteLine();
foreach (int i in stack.BottomToTop) Console.Write("{0} ", i);
Console.WriteLine();
}
}
当然,这些属性还可以用在foreach语句的外面。下面的例子将调用属性的结果传递给一个独立的Print方法。这个例子还展示了一个迭代器被用作一个带参的FromToBy方法的方法体:
using System;
using System.Collections.Generic;
class Test
{
static void Print(IEnumerable collection) {
foreach (int i in collection) Console.Write("{0} ", i);
Console.WriteLine();
}
static IEnumerable FromToBy(int from, int to, int by) {
for (int i = from; i <= to; i += by) {
yield return i;
}
}
static void Main() {
Stack stack = new Stack();
for (int i = 0; i < 10; i++) stack.Push(i);
Print(stack.TopToBottom);
Print(stack.BottomToTop);
Print(FromToBy(10, 20, 2));
}
}
这个例子的输出为:
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
10 12 14 16 18 20
泛型和非泛型的可枚举接口都只有一个单独的成员,一个无参的GetEnumerator方法,它返回一个枚举器接口。一个可枚举接口很像一个枚举器工厂(enumerator factory)。每当调用了一个正确地实现了可枚举接口的类的GetEnumerator方法时,都会产生一个独立的枚举器。
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable FromTo(int from, int to) {
while (from <= to) yield return from++;
}
static void Main() {
IEnumerable e = FromTo(1, 10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3} ", x * y);
}
Console.WriteLine();
}
}
}
上面的代码打印了一个从1到10的简单乘法表。注意FromTo方法只调用了一次用来产生可枚举接口e。而e.GetEnumerator()被调用了多次(通过foreach语句)来产生多个相同的枚举器。这些枚举器都封装了FromTo声明中指定的代码。注意,迭代其代码改变了from参数。不过,枚举器是独立的,因为对于from参数和to参数,每个枚举器拥有它自己的一份拷贝。在实现可枚举类和枚举器类时,枚举器之间的过渡状态(一个不稳定状态)是必须消除的众多细微瑕疵之一。C#中的迭代器的设计可以帮助消除这些问题,并且可以用一种简单的本能的方式来实现健壮的可枚举类和枚举器类。
理解和掌握泛型编程为我们更方便的写出稳定、安全的程序提供了便捷的方式。它将是未来C#发展的一个重要方面,会为C#乃至.Net的发展做出里程碑意义的贡献。
请发表评论