本章的主要内容如下:
● 泛型概述
● 创建泛型类
● 泛型类的特性
● 泛型接口
● 泛型结构
● 泛型方法
目录
1.1 泛型概述
1.1.1 性能
1.1.2 类型安全
1.1.3 二进制代码的重用
1.1.4 代码的扩展
1.1.5 命名约定
1.2 创建泛型类
1.3 泛型类的功能
1.3.1 默认值
1.3.2 约束
1.3.3 继承
1.3.4 静态成员
1.4 泛型接口
1.4.1 协变和抗变
1.4.2 泛型接口的协变
1.4.3 泛型接口的抗变
1.5 泛型结构
1.6 泛型方法
1.6.1 带委托的泛型方法
1.1 泛型概述
泛型是C#和.NET的一个重要概念。泛型不仅是C#编程语言的一部分,而且与程序集中的L(Intermediate Languag,中间语言)代码紧密地集成。有了泛型,就可以创建独立于被包含类型的类和方法。我们不必给不同的类型编写功能相同的许多方法或类,只创建一个方法或类即可。
另一个减少代码的选项是使用Object类,但使用派生自Object类的类型进行传递不是类型安全的。泛型类使用泛型类型,并可以根据需要用特定的类型替换泛型类型。这就保证了类型安全性:如果某个类型不支持泛类,编译器就会出现错误。
泛型不仅限于类,本章还将介绍用于接口和方法的泛型。用于委托的泛型参见第8章。
泛型不仅存在于C#中,其他语言中有类似的概念。例如,C+模板就与泛型相似。但是,C+模板和NET泛型之间有一个很大的区别。对于C+模板,在用特定的类型实例化模板时,需要模板的源代码。C十+编译器为每个属于特定模板实例的类型生成单独的二进制代码。相反,泛型不仅是C#语言的一种结构,而且是CLR(公共语言运行库定义的。所以,即使泛型类是在C#中定义的,也可以在Visual Basic中用一个特定的类型实例化该泛型。
下面介绍泛型的优点和缺点,尤其是:
● 性能
● 类型安全性
● 二进制代码重用
● 代码的扩展
● 命名约定
1.1.1 性能
泛型的一个主要优点是性能。第10章介绍了System.Collections和System.Collections. Generic命名空间的泛型和非泛型集合类。对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。
下面的例子显示了System.Collections命名空间中的ArrayList类。ArrayList存储对象, Add()方法定义为需要把一个对象作为参数,所以要装箱一个整数类型。在读取ArrayList中的值时,要进行拆箱,把对象转换为整数类型。可以使用类型转换运算符把ArrayList集合的第一个元素赋予变量i1,在访问int类型的变量i2的foreach语句中,也要使用类型转换运算符:
装箱和拆箱操作很容易使用,但性能损失比较大,迭代许多项时尤其如此。
System.Collections.Generic命名空间中的List<T>类不使用对象,而是在使用时定义类型。在下面的例子中,List<T>类的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进行装箱和拆箱操作。
1.1.2 类型安全
泛型的另一个特性是类型安全。与ArrayList类一样,如果使用对象,可以在这个集合中添加任意类型。下面的例子在ArrayList类型的集合中添加一个整数、一个字符串和一个MyClass类型的对象:
如果这个集合使用下面的foreach语句迭代,而该foreach语句使用整数元素来迭代,编译器就会编译这段代码。但并不是集合中的所有元素都可以转换为int,所以会出现一个运行异常:
错误应尽早发现。在泛型类List<T>中,泛型类型T定义了允许使用的类型。有了List<int>的定义,就只能把整数类型添加到集合中。编译器不会编译这段代码,因为Add()方法的参数无效:
1.1.3 二进制代码的重用
泛型允许更好地重用二进制代码。泛型类可以定义一次,用许多不同的类型实例化。不需要像C++模板那样访问源代码。
例如,System.Collections.Generic命名空间中的List<T>类用一个int、一个字符串和一个MyClass类型实例化:
泛型类型可以在一种语言中定义,在另一种.NET语言中使用。
1.1.4 代码的扩展
在用不同的类型实例化泛型时,会创建多少代码?
因为泛型类的定义会放在程序集中,所以用某个类型实例化泛型类不会在IL代码中复制这些类。但是,在JIT编译器把泛型类编译为内部码时,会给每个值类型创建一个新类。引用类型共享同一个内部类的所有实现代码。这是因为引用类型在实例化的泛型类中只需要4字节的内存单元(32位系统),就可以引用一个引用类型。值类型包含在实例化的泛型类的内存中。而每个值类型对内存的要求都不同,所以要为每个值类型实例化一个新类。
1.1.5 命名约定
如果在程序中使用泛型,区分泛型类型和非泛型类型会有一定的帮助。下面是泛型类型的命名规则:
● 泛型类型的名称用字母T作为前缀。
● 如果没有特殊的要求,泛型类型允许用任意类替代,且只使用了一个泛型类型,就可以用字符T作为泛型类型的名称。
● 如果泛型类型有特定的要求(例如必须实现一个接口或派生于基类),或者使用了两个或多个泛型类型,就应给泛型类型使用描述性的名称:、
1.2 创建泛型类
首先介绍一个一般的、非泛型的简化链表类,它可以包含任意类型的对象,以后再把这个类转化为泛型类。
在链表中,一个元素引用其后的下一个元素。所以必须创建一个类,将对象封装在链表中,引用下一个对象。类LinkedListNode包含一个对象value,它用构造函数初始化,还可以用Value属性读取。另外,LinkedListNode类包含对链表中下一个元素和上一个元素的引用,这些元素都可以从属性中访问。
LinkedList类包含LinkedListNode类型的first和last字段,它们分别标记了链表的头尾。AddLast()方法在链表尾添加一个新元素。首先创建一个LinkedListNode类型的对象。如果链表是空的,则first和last字段就设置为该新元素;否则,就把新元素添加为链表中的最后一个元素。执行GetEnumerator()方法时,可以用foreach语句迭代链表。GetEnumerator()方法使用yield语句创建一个枚举器类型。
现在可以给任意类型使用LinkedList类了。在下面的代码中,实例化了一个新LinkedList对象,添加了两个整数类型和一个字符串类型。整数类型要转换为一个对象,所以执行装箱操作,如前面所述。在foreach语句中执行拆箱操作。在foreach语句中,链表中的元素被强制转换为整数,所以对于链表中的第三个元素,会发生一个运行异常,因为它转换为int时会失败。
下面创建链表的泛型版本。泛型类的定义与一般类类似,只是要使用泛型类型声明。之后,泛型类型就可以在类中用作一个字段成员,或者方法的参数类型。LinkedListNode类用一个泛型类型T声明。字段value的类型是T,而不是object。构造函数和Value属性也变为接受和返回T类型的对象。也可以返回和设置泛型类型,所以属性Next和Prev的类型是LinkedListNode<T>。
1.3 泛型类的功能
在创建泛型类时,需要一些其他C#关键字。例如,不能把null赋予泛型类型。此时,可以使用default关键字。如果泛型类型不需要Object类的功能,但需要调用泛型类上的某些特定方法,就可以定义约束。
本节讨论如下主题:
● 默认值
● 约束
● 继承
● 静态成员
下面开始一个使用泛型文档管理器的示例。文档管理器用于从队列中读写文档。先创建一个新的控制台项目DocumentManager,添加类DocumentManager<T>。AddDocument()方法将一个文档添加到队列中。如果队列不为空,IsDocumentAvailable只读属性就返回true。
1.3.1 默认值
现在给DocumentManager<T>类添加一个GetDocument()方法。在这个方法中,给类型T指定null。但是,不能把null赋予泛型类型。原因是泛型类型也可以实例化为值类型,而null只能用于引用类型。为了解决这个问题,可以使用default关键字。通过default关键字,将null赋予引用类型,将0赋予值类型。
1.3.2 约束
如果泛型类需要调用泛型类型上的方法,就必须添加约束。对于DocumentManager<T>,文档的所有标题应在DisplayAllDocuments()方法中显示。Document类实现带有Title和Content只读属性的IDocument接口。
要使用DocumentManager<T>类显示文档,可以将类型T强制转换为IDocument接口,以显示标题:
问题是,如果类型T没有执行IDocument接口,这个类型转换就会生成一个运行异常。最好给DocumentManager<TDocument>类定义一个约束:TDocument类型必须执行IDocument接口。为了在泛型类型的名称中指定该要求,将T改为TDocument。where子句指定了执行IDocument接口的要求。
这样,就可以编写foreach语句,让类型T包含属性Title了。Visual Studio IntelliSense和编译器都会提供这个支持。
在Main()方法中,DocumentManager<T>类用Document类型实例化,而Document类型执行了需要的IDocument接口。接着添加和显示新文档,检索其中一个文档:
DocumentManager现在可以处理任何执行了IDocument接口的类。
在示例应用程序中,介绍了接口约束。泛型还有几种约束类型,如表9-1所示。
注意·:
在C#中,where子句的一个重要限制是,不能定义必须由泛型类型执行的运算符。运算符不能在接口中定义。在where子句中,只能定义基类、接口和默认构造函数。
1.3.3 继承
前面创建的LinkedList<T>类执行了IEnumerable<T>接口:
泛型类型可以执行泛型接口,也可以派生于一个类。泛型类可以派生于泛型基类:
其要求是必须重复接口的泛型类型,或者必须指定基类的类型,如下所示:
于是,派生类可以是泛型类或非泛型类。例如,可以定义一个抽象的泛型基类,它在派生类中用一个具体的类型实现。这允许对特定类型执行特殊的操作:
还可以创建一个部分的特殊操作,如从Quey中派生StringQuery类,只定义一个泛型参数,如字符事 TResult。要实例化StringQuery,只需要提供TRequest的类型:
1.3.4 静态成员
泛型类的静态成员需要特别关注。泛型类的静态成员只能在类的一个实例中共享。下面看一个例子,其中 StaticDemo<T>类包含静态字段x:
由于同时对一个string类型和一个int类型使用了StaticDemo<T>类,因此存在两组静态字段:
StaticDemo<string>.x = 4;
StaticDemo<int>.x = 5;
Console.WriteLine(StaticDemo<string>.x); // writes 4
1.4 泛型接口
使用泛型可以定义接口,接口中的方法可以带泛型参数。在链表示例中,就执行了IEnumerable<T>接口,它定义了GetEnumerator()方法,以返回IEnumerator<out T>。NET为不同的情况提供了许多泛型接口,例如,IComparable<T>、ICollection<T>和IExtensibleObject<T>。同一个接口常常存在比较老的非泛型版本,例如,.NET1.0有基于对象的IComparable接口。IComparable<inT>基于一个泛型类型:
比较老的非泛型接口IComparable需要一个带CompareTo0方法的对象。这需要强制转换为特定的类型,例如,Person类要使用LastName属性,就需要使用CompareToO方法:
执行泛型版本时,不再需要将object的类型强制转换为Person:
1.4.1 协变和抗变
在NET4之前,泛型接口是不变的。.NET4通过协变和抗变为泛型接口和泛型委托添加了一个重要的扩展。协变和抗变指对参数和返回值的类型进行转换.例如,可以给一个需要Shape参数的方法传送Rectangle参数吗?下面用示例说明这些扩展的优点。
在.NET中,参数类型是抗变的,假定有Shape和Rectangle类,Rectangle类派生自Shape基类。声明DisplayO方法是为了接受Shape类型的对象作为其参数:
现在可以传递派生自Shape基类的任意对象。因为Rectangle派生自Shape,所以Rectangle满足Shape的所有要求,编译器接受这个方法调用:
方法的返回类型是协变的。当方法返回一个Shape时,不能把它赋予Rectangle,因为Shape不一定总是 Rectangle。反过来是可行的:如果一个方法像GetRectangleO方法那样返回一个Rectangle,.
在NET Framework4版本之前,这种行为方式不适用于泛型。自C#4以后,扩展后的语言支持泛型接口和泛型委托的协变和抗变。下面开始定义Shape基类和Rectangle类.
1.4.2 泛型接口的协变
如果泛型类型用out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。接口IIndex与类型T是协变的,并从一个只读索引器中返回这个类型.
IIndex<T>接口用RectangleCollection类来实现。RectangleCollection类为泛型类型T定义了Rectangle:
RectangleCollection.GetRectangleO方法返回一个实现IIndex<Rectangle>接口的RectangleCollection类,所以可以把返回值赋予IIndex<Rectangle>类型的变量rectangle。因为接口是协变的,所以也可以把返回值赋予 Index<Shape>类型的变量。Shape不需要Rectangle没有提供的内容。使用shapes变量,就可以在for循环中使用接口中的索引器和Cont属性。
1.4.3 泛型接口的抗变
如果泛型类型用关键字标注,泛型接口就是抗变的。这样,接口只能把泛型类型T用作其方法的输入。
ShapeDisplay类实现Display<Shape>,并使用Shape对象作为输入参数
创建ShapeDisplay的一个新实例,会返回Display<Shape>,并把它赋予shapeDisplay变量.因为Display<T>是抗变的,所以可以把结果赋予Display<Rectangle>,其中Rectangle派生自Shape。这次接口的方法只能把泛型类型定义为输入,而Rectangle满足Shape的所有要求。
1.5 泛型结构
与类相似,结构也可以是泛型的。它们非常类似于泛型类,只是没有继承特性。本节介绍泛型结构 Nullable<-T>,它由.NET Framework定义。
NET Framework中的一个泛型结构是Nullable<T>。数据库中的数字和编程语言中的数字有显著不同的特征,因为数据库中的数字可以为空,而C#中的数字不能为空。32是一个结构,而结构实现同值类型,所以结构不能为空。这种区别常常令人很头痛,映射数据也要多做许多辅助工作。这个问题不仅存在于数据库中,也存在于把XML数据映射到.NET类型。
一种解决方案是把数据库和L文件中的数字映射为引用类型,因为引用类型可以为空值。但这也会在运行期间带来额外的系统开销。
使用Nullable<T>结构很容易解决这个问题。下面的代码段说明了如何定义Nullable<T>的一个简化版本。结构Nullable<T>定义了一个约束:其中的泛型类型T必须是一个结构。把类定义为泛型类型后,就没有低系统开销这个优点了,而且因为类的对象可以为空,所以对类使用Nullable<T>类型是没有意义的。除了Nullable<T>定义的T类型之外,唯一的系统开销是has Value布尔字段,它确定是设置对应的值,还是使之为空。除此之外,泛型结构还定义了只读属性Has Value和Vaue,以及一些运算符重载。把Nullable<T>类型强制转换为T类型的运算符重载是显式定义的,因为当has Value为false时,它会抛出一个异常。强制转换为Nullable<T>类型的运算符重载定义为隐式的,因为它总是能成功地转换:
在这个例子中,Nullable<T>用Nullable<int>实例化。变量x现在可以用作一个int,进行赋值或使用运算符执行一些计算。这是因为强制转换了Nullable<T>类型的运算符。但是,x还可以为空。Nullable<T>的Has Value和Value属性可以检查是否有一个值,该值是否可以访问:
因为可空类型使用得非常频繁,所以C#有一种特殊的语法,它用于定义可空类型的变量。定义这类变量时不使用泛型结构的语法,而使用“?”运算符。在下面的例子中,变量x1和x2都是可空的t类型的实例:
可空类型可以与null和数字比较,如上所示。这里,x的值与null比较,如果x不是null,它就与小于0的值比较:
知道了Nullable<T>是如何定义的之后,下面就使用可空类型。可空类型还可以与算术运算符一起使用。变量x3是变量x1和x2的和。如果这两个可空变量中任何一个的值是null,它们的和就是null。
注意:这里调用的GetNullableType()方法只是一个占位符,它对于任何方法都返回一个可空的int。为了进行测试,简单起见,可以使实现的GetNullableTypeO返回null或返回任意整数。
非可空类型可以转换为可空类型。从非可空类型转换为可空类型时,在不需要强制类型转换的地方可以进行隐式转换。这种转换总是成功的:
但从可空类型转换为非可空类型可能会失败。如果可空类型的值是null,并且把null值赋予非可空类型就会抛出nvalidOperationException类型的异常。这就是需要类型强制转换运算符进行显式转换的原因:
如果不进行显式类型转换,还可以使用合并运算符从可空类型转换为非可空类型。合并运算符的语法是“?”,为转换定义了一个默认值,以防可空类型的值是null。这里,如果x1是null,y1的值就是0。
1.6 泛型方法
除了定义泛型类之外,还可以定义泛型方法。在泛型方法中,泛型类型用方法声明来定义。
Swap<T>方法把T定义为泛型类型,用于两个参数和一个变量temp:
把泛型类型赋予方法调用,就可以调用泛型方法:
但是,因为C#编译器会通过调用Swap方法来获取参数的类型,所以不需要把泛型类型赋予方法调用。泛型方法可以像非泛型方法那样调用:
1.6.1 带委托的泛型方法
这个Accumulate(0方法使用两个泛型参数T1和T2,第一个参数T1用于实现Enumerable<T1>参数的集合,第二个参数使用泛型委托FncT1,T2,TResult>。其中,第2个和第3个泛型参数都是T2类型。需要传递的方法有两个输入参数(T1和T2)和一个T2类型的返回值。
在调用这个方法时,需要指定泛型参数类型,因为编译器不能自动推断出该类型。对于方法的第1个参数,所赋予的accounts集合是Enumerable<Account>类型。对于第2个参数,使用一个lambda表达式来定义Account和decimal类型的两个参数,返回一个小数。对于每一项,通过Accumulate02方法调用这个lambda表达式。
不要为这种语法伤脑筋。该示例仅说明了扩展Accumulate()方法的可能方式。