李凯凯 闵联营
(武汉理工大学计算机科学与技术学院 武汉 430063)
在CLR(common language runtime)1.0中,当要创建一个灵活的类或方法,但该类或方法在编译期间不知道使用什么类,就必须以System.Object类为基础进行处理,而Object类在编译期间没有类型安全性,又必须进行强制类型转换.另外,给值类型使用Object类会有性能损失,这给程序开发带来诸多不便.
故在CLR 2.0(.NET 3.5基于 CLR 2.0)中,提供了泛型,这是一个很强大的新特性.有了泛型,就不再需要Object类来参与实现一些通用类或方法了.通过使用泛型类型,可以根据需要,用特定的类型替换泛型类型,同时保证了类型安全性:如果某个类型不支持泛型类,编译器就会报错,以阻止程度发生运行期错误.正确使用泛型将大大提高代码的灵活性,结合一个优秀的设计模式[1],可以显著缩短开发时间.
C#是一个类型安全的语言[2],类型安全允许编译器(可信赖的)捕获潜在的错误,而不是在程序运行时才发现(不可信赖的).在CLR 1.0中,当使用集合时,这种类型安全就失效了:由.NET类库提供的集合类全是存储基类型(Object)的,而.NET中所有的一切都继承于Object,因此所有类型都可以放到一个集合中,这相当于根本就没有了类型检测.
下面的代码也正好说明了这个问题.
ArrayList list=new ArrayList();
list.Add(100);list.Add(“test”);list.Add(new object());
foreach(int i in list)// 引发运行期错误
Console.Write(i);
可以往ArrayList里添加任何类型,也能通过编译,但是在接下来的使用中,由于字符串“test”和Object对象都不能转换为值类型int,这会抛出运行期错误,类型不再安全.
泛型的一个主要优点是性能.如果对值类型使用普通的集合类,在把值类型转换为引用类型和把引用类型转换成为值类型时,程序会进行装箱和拆箱操作,性能损失比较大,操作迭代多次时尤其严重.而使用泛型能使程序在运行期间明确知道操作的对象的类型,可以减少拆箱和装箱的操作.
ArrayList list=new ArrayList();
list.Add(100);//装箱:将值类型转换为引用类型
int i= (int)list[0];//拆箱:将引用类型转换为值类型
foreach(int j in list)//拆箱
Console.Write(j);
List<int>list=new List<int>();
list.Add(100);//无装箱:类型已经存储在 List<int>
int i=list[0];//无拆箱:不需要进行类型转换
foreach(int j in list)
Console.Write(j);
泛型允许更好地重用代码.泛型类可以只定义一次,用于不同的类型实例化,减少代码量,如List<int>,List<string>,List<object>等等,且泛型可以在一种语言中定义,在另一种.NET语言中使用(如C#,VB.NET等).
泛型编译为IL(intermediate language)代码时,是采用占位符来表示泛型类型,并用专有的IL指令支持泛型操作,所以用某个类型实例化泛型不会在IL代码中复制这些类.为了说明,使用一个最简单的泛型类class Test<T>{…},编译运行后,使用Visual Studio自带的IL反汇编程序打开生成的可执行文件,定位到Test<T>类的构造函数,可以看到下面的代码:
可以看出,泛型类使用了占位符“T”来表示这个泛型所支持的类型参数,并不会生成多份Test类以适应不同的传入类型.
真正的泛型实例化工作以“on-demand”的方式[3],发生在JIT(Just-in-time)编译时,CLR 为所有类型参数为“引用类型”的泛型类产生同一份代码;但是如果类型参数为“值类型”,对每一个不同的“值类型”,CLR将为其产生一份独立的代码.
C#中的值类型必须包含一个值,而引用类型可以为空(null),但是让值类型可空是非常有用的(配合数据库使用).所以,.NET提供了泛型System.Nullable<T>可以使值类型具备可空的性质,其中类型参数T必须是不可以为null的类型.因为可空类型使用得非常频繁,所以C#有一种特殊的语法,使用“?”运算符,用于定义这种类型的变量,如int?.
System.Collection.Generic命名空间下有大量泛型集合类[4],图1列示了几个比较常用的泛型,它们使用起来都十分方便,.NET已经为这些类提供了完善的成员函数与属性.
图1 泛型集合类
3.1.1 定义泛型类 泛型类的定义使用了占位符来泛化用到的类型,例如使用类型参数T来定义一个泛型类:
这个类现在可以根据T接受其他实例类型,并且生成的类是强类型的.
3.1.2 定义泛型的误区 在定义泛型时需要注意,由于T的类型是未知的,如果不使用其他一些技术(如反射、约束等)[5],就不能对T 进行一些关于T实际类型相关的操作.当需要对泛型参数的实例进行初始化时,可以使用“default”关键字,如T1innerObj=default(T1),这会给inner-Obj赋予对应类型的默认值.另外,与普通类不同,泛型类的静态成员只能在类的一种实例类型中共享.
没有对其进行任何约束的泛型称之为无绑定(unbounded)类型,而通过约束类型,可以限制泛型类的实例化类型参数.约束是在泛型类定义的时候,使用关键字where来实现:
其中:constraint定义了约束的种类(见表1),不同的约束之间用逗号隔开,多个where语句用空格隔开,且约束必须出现在继承说明符的后面.
表1 常用的约束
通过泛型约束,可以在泛型内调用泛型参数T上的方法,访问其成员,使代码更具操作性,泛型约束对于程序开发有非常重要的意义.
.NET已有的泛型的功能已经比较完善,但是如果想在其基础上增加自定义的操作,可以定义一个泛型类并继承.NET已有的泛型,见图2.
图2 泛型继承关系示例
需要注意的是,如果某个类型在它所继承的基类型中受到约束,该类型就不能“解除约束”.也就是说,类型参数T在基类中使用受到了某约束S,则在子类中T必须受到至少与基类型相同的约束.
经常用到的还包括泛型结构、方法、接口、委托等,它们的使用方法与非泛型版本基本相同,只是在其基础上增加了类型参数、约束等特性.
C#泛型和C++的模板都是用于提供参数化类型支持的语言功能,然而,两者之间存在许多差异.在语法层面上,C#泛型是实现参数化类型的更简单方法,不具有C++ 模板的复杂性[6].
在CLR中,泛型类型或方法将被编译为中间语言 MSIL (microsoft intermediate language),它包含将其标识为具有类型参数的元数据.元数据描述代码中的类型,包括每种类型的定义、每种类型的成员的签名、代码引用的成员和运行库在执行时使用的其他数据.C#泛型类型替换是在运行时执行的,从而为实例化的对象保留了泛型类型信息.而C++模板是在编译时就确定了所需实例化的对象,代码的实例化发生在程序运行之前.在C++里,可以对一个类型参数T做任何想做的事情,但是当进行实例化的时候,有可能会出现运行期错误,并得到一些非常难懂的错误信息.比如,有2个T类型的变量x和y,如果要在代码中完成x+y的操作,那么需要先定义用于2个T型变量相加的“+”运算符,否则会得到一些古怪的错误信息.从某种意义上说,C++模板实际上是非类型化的,或者说是弱类型化的,而C#泛型则是强类型化的.
此外,C#并不提供C++模板所提供的所有功能.C#泛型未提供与C++模板相同程度的灵活性:不允许非类型模板参数,如template C<int i> {};不支持显式专用化,即特定类型的模板的自定义实现;不支持部分专用化,类型参数子集的自定义实现;不允许将类型参数用作泛型类型的基类;不允许类型参数具有默认类型.在C#中,尽管构造类型可用作泛型,但泛型类型参数自身不能是泛型,C++则允许模板参数.
另一方面,C++模板允许那些可能并非对模板中的所有类型参数都有效的代码,然后将检查该代码中是否有用作类型参数的特定类型.C#则要求相应地编写类中的代码,使之能够使用任何满足约束的类型.如图3所示的C++代码,可以直接对类型参数的对象使用算术运算符+和-,但在用不支持这些运算符的类型来实例化模板时将会产生错误;而C#不允许这样(图3中的a+b运算将会产生编译错误),惟一允许的语句表达是那些可从约束推导出来的构造.
图3 C++模板与C#泛型
所以说,C++模板会给设计者带来了很大的好处,在功能实现上有很大的自由度.但是,C#泛型更为安全,这对泛型的使用者是有利的,并且保证了传递给运行时用于类型构造的任何泛型都是正确的,在工程项目中能提供更为可靠的代码段.
Java的泛型是Java 5之后才有的特性,它的使用与C#泛型虽大致相同,但本质却大不相同.
Java泛型实际上是通过类型擦除(type erasure)在Java语言编译器上实现的,而JVM(java virtual machine)本身并没有“泛型”的概念,这样最大的优势在于其兼容性:即便使用了泛型,但最后生成的二进制文件也可以运行在低版本的JVM 上,甚至JDK(java development kit)中都不需要添加额外的类库,因为Java的泛型不涉及JVM的变化.而.NET中的泛型是得到了CLR的支持[7],在运行库上实现,这可以在运行期间体现出“模板化”的优势,具有很高的效率.
如果试图创建一个List<int>,就会对所有用到的int对象进行装箱操作,这会产生很大的效率问题.此外,为了与旧版本的JVM兼容,实现强类型,Java编译器实际上还会插入各种各样的转换代码,当然这些转换代码是由Java编译程序来完成的.也就是说Java泛型只是在语法上增加了易用性,但是并没有提升任何程序执行上的效率.图4的例子清楚地说明了两者之间的效率差距,在相同的机器上使用图4定义的List<T>类做10 000×10 000次累加操作时,Java泛型版本的平均耗时在1 380ms,而C#版本的平均耗时仅为350ms.
图4 泛型效率对比
同时,Java泛型还有一个更大的问题,因为Java泛型的实现依赖于类型擦除,到了代码运行时,程序实际上得不到一个相对于运行时的可靠的泛型表示.比如,在Java里针对一个泛型List使用反射的时候,程序并不知道这个List到底是关于什么类型的List,它只是一个List.因为编译器已经丢掉了类型信息,任何动态代码生成(dynamic code-generation)的应用或者基于反射的应用会无法正常工作.
所以说,Java泛型更相当于一个“伪泛型”,虽然兼容性比较好,但是运行效率却没有任何提升,甚至有所下降.而对于C#泛型,CLR会为不同的泛型类型生成不同的具体类型代码(类型膨胀),能够节省值类型的装箱和拆箱的开销,即便是引用类型也可以避免额外的类型转化,这些都能带来性能上的提高.而且由于C#泛型编译后的MSIL代码中含有类型参数的信息,这就允许程序员能以更丰富的方式来使用泛型.
综上所述,正确地使用.NET泛型,能够提高编程效率、改善代码结构、加强程序安全.虽然C#泛型的功能不及C++模版强大,但是它的安全性更高,可靠性更好.而Java“伪泛型”无论是在效率、性能还是安全性上都不及C#泛型.总之.NET泛型具有高性能、高安全和良好重用性的优秀品质,在.NET应用开发中,泛型将一直占据非常重要的地位.
[1]谈 冉,陈 巍,薛胜军.设计模式在典型.NET三层架构Web程序中的应用[J].武汉理工大学学报:交通科学与工程版,2006,30(2):344-346.
[2]CHEN Chaochao,DOUGLAS B,CHRIS S,et al.A.NET framework for an integrated fault diagnosis and failure prognosis architecture[C]//2010IEEE Autotestcon Conference.Orlando,USA:36-41.
[3]NAGEL C,EVJEN B,GLYNN J,et al.Professional C#2010[M].7th ed.[s.l.]:Wrox,2010.
[4]姜 宇,牟永敏.C#2.0泛型集合的应用研究[J].微计算机信息,2010,26(27):171-174.
[5]WATSON K,NAGEL C,PEDERSEN J H,et al.Beginning microsoft visual C#2010[M].5th ed.[s.l.]:Wrox,2010.
[6]陈 林,徐宝文.基于源代码静态分析的C++0x泛型概念 抽 取 [J].计 算 机 学 报,2009,32(9):1792-1803.
[7]谢 伟.C#中的泛型[J].科学咨询:科技·管理,2011,12(7):91-92.