本文将为大家讲述.NET中最常见的一种特性,C#泛型。希望通过本文能帮助大家更好的学习和理解C#泛型,在平时的开发工作中起到事半功倍的效果。
泛型,.NET的这个特性相信大家都已经很熟悉了,提起泛型,不能不首先提到C++中的模板,C++中模板的引入大大提高了代码的重用性,因此也得到了许多程序员的喜爱。因此,在同为强类型语言平台的.NET 2.0和Java 1.5中,它们也都不约而同的引入了泛型的对语言和平台的支持。不过虽然三种语言最终都提供了将类型参数化的功能,然而这个功能在三个平台或语言中的实现却大大不同。相对来说,C++的模板功能是三者中最为强大的,不过由于.Net和Java对类型安全和稳定性要求更高,它们对泛型的支持要稍微简单,不过即使如此,二者对泛型特性的实现也引起了两个阵营中程序员们的争论,不过最终普遍认为Java的伪泛型(擦拭法)要比.NET的JIT级别的真正的泛型性能要差(java仍然有装箱,拆箱操作)。当然这些是后话,下面我们来看看.NET的泛型到底如何使用吧!
基本介绍
.NET 2.0以后以后支持在很多类型上使用泛型,包括类、结构、接口、委托和方法成员,在这些类型上使用泛型和在类上使用是一样的。它甚至支持同一个接口但不同泛型类型的实现,这有点类似重载在类级别的实现。最后.NET允许你同时定义多个泛型类型。
在泛型方法中的泛型类型基本跟在类中使用情况一样,不过泛型方法有一个方便程序员的地方就是它的类型推断功能,这意味着程序员可以即能和使用普通方法一样使用这些方法,同时又能享受泛型带来的方便。e.g.
代码
static void Test<T, U>(T t, U u) { }static void main(){ //在函数中我们可以不用声明参数类型,编译器会自动根据实际数据 //自动推断类型 Test(10, "20");Test(1.1, 2.2);} |
下面我们来看看泛型在.NET中使用的一些需要注意的地方。
1. 泛型在嵌套类中的使用。嵌套的子类会自动继承(?)包裹类的泛型类型,当然,你也可以在嵌套类中覆盖掉包裹类的类型,不过编译器会在编译的时候发出警告来提醒用户注意避免误写。e.g.
class Container<T, U>{ //编译器会在这里发出警告 //告诉用户这里的泛型和包裹类相同 class Nested<U>{ void Method(T p0, U p1) { }}} |
2. 协变和逆变的问题。关于协变和逆变的定义简单来说就是泛型类型是否允许子类和父类之间转换,这里不做详细讨论,读者如果有兴趣可以参考这篇文章。在.net 4.0以前是不支持协变和逆变的,这也让我们的代码有些时候实现起来很别扭。下面可以看个简单的例子(注:这个例子仅作说明用,不一定恰当)。
首先我们定义两个数据类型,IData和IOperation:
interface IOperation where |
然后我们分别定义不同类型的数据和操作类:
代码
class AddData : IData{public int A1, A2; public void method() { }}class Add : IOperation<AddData> {public void Run(AddData d) {Console.WriteLine(d.A1 + d.A2);}} class ComplexData : IData{public void method() { } public int A1, A2, B1, B2;} class ComplexAdd : IOperation<ComplexData>{ public void Run(ComplexData d){Console.WriteLine("{0}+{1} i",d.A1 + d.A2,d.B1+d.B2);}} |
这里如果能这样使用我们认为应该是安全的:
IOperation<IData> opr = new Add();opr.Run(data1); opr = new ComplexAdd();opr.Run(data2); |
然而这样的代码是无法通过编译的,尽管我们知道它们的使用绝对安全的,因为AddData或ComplexData是IData的子类。幸运的是,在.Net4.0中程序员将不会有这个烦恼了。
3. 泛型不支持操作符。在C++中模板支持操作符,然而,由于操作符是静态的并且是编译时决定的(参看这篇文章),因此作为运行时的泛型无法实现类型间的该项操作,虽然你可以通过接口来达到同样功能,但方便的操作符终究无法在泛型中得到支持。这可以算是C#泛型的一个缺点,因为在很多时候它确实很有用。
4. 泛型的类型转换问题。泛型无法从其他类型(object除外)直接强制转换,这个时候如果需要将其他类型转换为泛型对象时有两种方式,一种是该泛型约束是class或基类,这时候可以通过as 操作符来转换,如 return somevalue as T。但是有时候如果我们不知道该泛型的类型或者该泛型类型是struct该如何转换呢?答案是通过两次类型转换,首先我们把待转换对象转换为object对象,然后直接对该object对象强制转换为T,e.g. return (T)(object)someVar。具体例子你可以参考这篇文章。
最后,在泛型中有个关键字--default,顾名思义,它是在引用类型和值类型没有初始化的时候提供默认值的。对引用类型默认值是null,值类型则是0.
泛型约束
如果.Net仅仅出现泛型而没有泛型约束,我想泛型的功能一定会大打折扣的,正是有了泛型约束,才让我们在操作这些类型更加规范和准确。这也是同为强类型的C#比C++的模板更安全的一点。
和类声明继承关系时一样,泛型约束可以声明多个接口和最多一个基类约束,并且如果声明了基类约束,类约束必须放在约束条件的首位,这和我们声明类的继承关系要求一样。另外,声明约束的类不能是密封类或某些特殊的结构(如Nullable),如我们不能声明约束类为string或System.Nullable.最后,与我们在类声明多个接口继承关系一样,泛型的约束间是AND而非OR关系,也就是说,如果你添加了多个约束,那么泛型使用必须满足所有的约束条件。
我们可以通过关键字class和struct来限定类型是值类型还是引用类型,不过由于基类约束已经表明了泛型类型是类还是结构,所以我们不能同时将class或struct约束和基类(结构)约束一起使用,e.g.class ClassAwhere T:BaseClass,class 是不允许的。另外一个需要注意的就是class和struct约束也必须在其他任何约束条件之前。
另外一个值得注意的约束关键字是new(), new 关键字意味着泛型对象必须提供一个无参构造函数,需要注意的是,new()约束必须放在所有约束的最后面。这个约束有时会有用,不过有时看起来更像鸡肋。首先,new()约束虽然表明你可以在类中对泛型对象使用new()操作符实例化对象,然而在CIL对该对象的实例化仍然是通过反射来实现的,即T a=new T()相当于T a = System. Activator. CreateInstance();这样程序效率会有所降低。另一方面,目前new约束仅仅支持无参构造函数的约束,而无法支持用户自定义参数的构造函数约束,虽然用户可以自己通过工厂方法来传递参数,但终究不够自由,这让new()约束有时没太大用武之地。
约束不支持委托和枚举类型,例如,你不能这样定义:class ClassA where T:Delegate. 这是由于委托和枚举被认为是特殊的类,它无法被指定为类型参数。编译器无法根据Delegate来完成编译器的类型检查。
最后类型约束支持继承,但同时你必须在子类定义泛型的时候再重新声明一遍父类的所有约束。设计者的出发点是让程序员能清楚子类中约束从何而来,减少疑惑。但从另外个角度来讲,这样反而会让程序员不得不多添加一些重复的代码,即使你已经知道它的约束条件都有哪些。
泛型内部实现
泛型在.NET中真正做到了平台级别的支持,在C#中,泛型同样是对象。事实上,编译器会在编译的时候将泛型参数转换为特殊的元数据,CLR会根据需要生成其实际的类型。为避免装箱和拆箱,值类型的泛型实现和引用类型的是不一样的。下面我们来具体看看它们有和不同。
1. 值类型的泛型对象实例化
第一次用值类型作为参数来构造泛型类型时,运行库会创建专用泛型类型,将提供的参数代入到 MSIL 中的适合位置。对于每个用作参数的唯一值类型,都会创建一次专用C# 泛型类型。这种特定类型的泛型类其实就相当于包含特定值类型的本地代码,它将对性能提升很有帮助。
2. 引用类型的泛型对象实例化
对于引用类型,泛型的工作方式略有不同。第一次使用任何引用类型构造泛型类型时,运行库会创建专用泛型类型。用对象引用(或者说指针更好)替换MSIL中的参数.然后,每次使用对象的引用作为参数来实例化。构造类型时,无论引用类型的详细类型是什么,运行库都会重用以前创建的泛型类型的专用版本。之所以可以这样, 是因为所有对象引用的大小相同 。
总结
在.NET类库中处处都可以看到泛型的身影,尤其是数组和集合中,泛型的存在也大大提高了程序员的开发效率。更重要的是,C#的泛型比C++的模板使用更加安全,并且通过避免装箱和拆箱操作来达到性能提升的目的。因此,我们很有必要掌握并善用这个强大的语言特性。