无私分享(C#高级编程第6版doc):第09章 泛型
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
目录
第9章泛型 (2)
9.1 概述 (2)
9.1.1 性能 (2)
9.1.2 类型安全 (3)
9.1.3 二进制代码的重用 (3)
9.1.4 代码的扩展 (4)
9.1.5 命名约定 (4)
9.2 创建泛型类 (4)
9.3 泛型类的特性 (8)
9.3.1 默认值 (9)
9.3.2 约束 (9)
9.3.3 继承 (11)
9.3.4 静态成员 (12)
9.4 泛型接口 (12)
9.5 泛型方法 (13)
9.6 泛型委托 (15)
9.6.1 执行委托调用的方法 (15)
9.6.2 对Array类使用泛型委托 (17)
9.7 Framework的其他泛型类型 (19)
9.7.1 结构Nullable<T> (19)
9.7.2 EventHandler<TEventArgs> (20)
9.8 小结 (21)
第9章泛型
CLR 2.0的一个新特性是泛型。
在.CLR 1.0中,要创建一个灵活的类或方法,但该类或方法在编译期间不知道使用什么类,就必须以Object类为基础。
而Object类在编译期间没有类型安全性,因此必须进行强制类型转换。
另外,给值类型使用Object类会有性能损失。
CLR 2.0(.NET 3.5基于CLR 2.0)提供了泛型。
有了泛型,就不再需要Object类了。
泛型类使用泛型类型,并可以根据需要用特定的类型替换泛型类型。
这就保证了类型安全性:如果某个类型不支持泛型类,编译器就会生成错误。
泛型是一个很强大的特性,对于集合类而言尤其如此。
.NET 1.0中的大多数集合类都基于Object 类型。
.NET 从2.0开始提供了实现为泛型的新集合类。
泛型不仅限于类,本章还将介绍用于委托、接口和方法的泛型。
本章的主要内容如下:
●泛型概述
●创建泛型类
●泛型类的特性
●泛型接口
●泛型方法
●泛型委托
● Framework的其他泛型类型
9.1 概述
泛型并不是一个全新的结构,其他语言中有类似的概念。
例如,C++模板就与泛型相当。
但是,C++模板和.NET泛型之间有一个很大的区别。
对于C++模板,在用特定的类型实例化模板时,需要模板的源代码。
相反,泛型不仅是C#语言的一种结构,而且是CLR定义的。
所以,即使泛型类是在C#中定义的,也可以在Visual Basic中用一个特定的类型实例化该泛型。
下面介绍泛型的优点和缺点,尤其是:
●性能
●类型安全性
●二进制代码重用
●代码的扩展
●命名约定
9.1.1 性能
泛型的一个主要优点是性能。
第10章介绍了System.Collections和System.Collections. Generic命名空间的泛型和非泛型集合类。
对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。
注意:
装箱和拆箱详见第6章,这里仅简要复习一下这些术语。
值类型存储在堆栈上,引用类型存储在堆上。
C#类是引用类型,结构是值类型。
.NET很容易把值类型转换为引用类型,所以可以在需要对象(对象是引用类型)的任意地方使用值类型。
例如,int可以赋予一个对象。
从值类型转换为引用类型称为装箱。
如果方法需要把一个对象作为参数,而且传送了一个值类型,装箱操作就会自动进行。
另一方面,装箱的值类型可以使用拆箱操作转换为值类型。
在拆箱时,需要使用类型转换运算符。
下面的例子显示了System.Collections命名空间中的ArrayList类。
ArrayList存储对象,Add()方法定义为需要把一个对象作为参数,所以要装箱一个整数类型。
在读取ArrayList中的值时,要进行拆箱,把对象转换为整数类型。
可以使用类型转换运算符把ArrayList集合的第一个元素赋予变量i1,
在访问int
System.Collections.Generic命名空间中的List<T>类不使用对象,而是在使用时定义类型。
在下面的例子中,List<T>类的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进
9.1.2 类型安全
泛型的另一个特性是类型安全。
与ArrayList类一样,如果使用对象,可以在这个集合中添加任意类型。
下面的例子在ArrayList类型的集合中添加一个整数、一个字符串和一个MyClass类型的对象:
如果这个集合使用下面的foreach语句迭代,而该foreach语句使用整数元素来迭代,编译器就会编译这段代码。
但并不是集合中的所有元素都可以转换为int,所以会出现一个运行异常:
List<int>的定
9.1.3 二进制代码的重用
泛型允许更好地重用二进制代码。
泛型类可以定义一次,用许多不同的类型实例化。
不需要像C++模板那样访问源代码。
例如,System.Collections.Generic命名空间中的List<T>类用一个int、一个字符串和一个MyClass
类型实例化:
泛型类型可以在一种语言中定义,在另一种.NET语言中使用。
9.1.4 代码的扩展
在用不同的类型实例化泛型时,会创建多少代码?
因为泛型类的定义会放在程序集中,所以用某个类型实例化泛型类不会在IL代码中复制这些类。
但是,在JIT编译器把泛型类编译为内部码时,会给每个值类型创建一个新类。
引用类型共享同一个内部类的所有实现代码。
这是因为引用类型在实例化的泛型类中只需要4字节的内存单元(32位系统),就可以引用一个引用类型。
值类型包含在实例化的泛型类的内存中。
而每个值类型对内存的要求都不同,所以要为每个值类型实例化一个新类。
9.1.5 命名约定
如果在程序中使用泛型,区分泛型类型和非泛型类型会有一定的帮助。
下面是泛型类型的命名规则:
●泛型类型的名称用字母T作为前缀。
●如果没有特殊的要求,泛型类型允许用任意类替代,且只使用了一个泛型类型,就可以用字符T
●
型类型,就应给泛型类型使用描述性的名称:
9.2 创建泛型类
首先介绍一个一般的、非泛型的简化链表类,它可以包含任意类型的对象,以后再把这个类转化为泛型类。
在链表中,一个元素引用其后的下一个元素。
所以必须创建一个类,将对象封装在链表中,引用下一个对象。
类LinkedListNode包含一个对象value,它用构造函数初始化,还可以用Value属性读取。
另外,LinkedListNode类包含对链表中下一个元素和上一个元素的引用,这些元素都可以从属性中访问。
LinkedList类包含LinkedListNode类型的first和last字段,它们分别标记了链表的头尾。
AddLast()方法在链表尾添加一个新元素。
首先创建一个LinkedListNode类型的对象。
如果链表是空的,则first和last字段就设置为该新元素;否则,就把新元素添加为链表中的最后一个元素。
执行GetEnumerator()方法时,可以用foreach语句迭代链表。
GetEnumerator()方法使用yield语句创建一个枚举器类型。
提示:
yield
现在可以给任意类型使用LinkedList类了。
在下面的代码中,实例化了一个新LinkedList对象,添加了两个整数类型和一个字符串类型。
整数类型要转换为一个对象,所以执行装箱操作,如前面所述。
在foreach语句中执行拆箱操作。
在foreach语句中,链表中的元素被强制转换为整数,所以对于
下面创建链表的泛型版本。
泛型类的定义与一般类类似,只是要使用泛型类型声明。
之后,泛型类型就可以在类中用作一个字段成员,或者方法的参数类型。
LinkedListNode类用一个泛型类型T声明。
字段value的类型是T,而不是object。
构造函数和Value属性也变为接受和返回T类型的对象。
下面的代码把LinkedList类也改为泛型类。
LinkedList<T>包含LinkedListNode<T>元素。
LinkedList 中的类型T定义了类型T的包含字段first和last。
AddLast()方法现在接受类型T的参数,实例化LinkedListNode<T>类型的对象。
IEnumerable接口也有一个泛型版本IEnumerable<T>。
IEnumerable<T>派生于IEnumerable,添加了返回IEnumerator<T>的GetEnumerator()方法,LinkedList<T>执行泛型接口IEnumerable<T>。
提示:
枚举、接口
使用泛型类LinkedList<T>,可以用int类型实例化它,且无需装箱操作。
如果不使用AddLast()方法传送int,就会出现一个编译错误。
使用泛型IEnumerable<T>,foreach语句也是类型安全的,如果foreach
同样,可以给泛型LinkedList<T>使用string类型,将字符串传送给AddLast()方法。
提示:
每个处理对象类型的类都可以有泛型实现方式。
另外,如果类使用了继承,泛型非常有助于去除类型转换操作。
9.3 泛型类的特性
在创建泛型类时,需要一些其他C#关键字。
例如,不能把null赋予泛型类型。
此时,可以使用default关键字。
如果泛型类型不需要Object类的功能,但需要调用泛型类上的某些特定方法,就可以定义约束。
本节讨论如下主题:
●默认值
●约束
●继承
●静态成员
下面开始一个使用泛型文档管理器的示例。
文档管理器用于从队列中读写文档。
先创建一个新的控制台项目DocumentManager,添加类DocumentManager<T>。
AddDocument()方法将一个文档添加到队列中。
如果队列不为空,IsDocumentAvailable只读属性就返回true。
9.3.1 默认值
现在给DocumentManager<T>类添加一个GetDocument()方法。
在这个方法中,给类型T指定null。
但是,不能把null赋予泛型类型。
原因是泛型类型也可以实例化为值类型,而null只能用于引用类型。
为了解决这个问题,可以使用default关键字。
通过default关键字,将null赋予引用类型,将0赋予值类型。
注意:
default关键字根据上下文可以有多种含义。
switch语句使用default定义默认情况。
在泛型中,根据泛型类型是引用类型还是值类型,default关键字用于将泛型类型初始化为null或0。
9.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所示。
表9-1
注意:
在CLR 2.0中,只能为默认构造函数定义约束,不能为其他构造函数定义约束。
使用泛型类型还可以合并多个约束。
where T : IFoo,new()约束和MyClass<T>声明指定,类型T 必须执行IFoo
提示:
在C#中,where子句的一个重要限制是,不能定义必须由泛型类型执行的运算符。
运算符不能在接口中定义。
在where子句中,只能定义基类、接口和默认构造函数。
9.3.3 继承
前面创建的LinkedList<T>类执行了IEnumerable<T>接口:
9.3.4 静态成员
泛型类的静态成员需要特别关注。
泛型类的静态成员只能在类的一个实例中共享。
下面看一个例子。
StaticDemo<T>类包含静态字段x:
由于对一个
9.4 泛型接口
使用泛型可以定义接口,接口中的方法可以带泛型参数。
在链表示例中,就执行了IEnumerable<T>接口,它定义了GetEnumerator()方法,以返回IEnumerator<T>。
对于.NET 1.0中的许多非泛型接口,.NET 从2.0开始定义了新的泛型版本,例如IComparable<T>:
第5
人员排序:
执行泛型版本时,不再需要将object的类型强制转换为Person:
9.5 泛型方法
除了定义泛型类之外,还可以定义泛型方法。
在泛型方法中,泛型类型用方法声明来定义。
Swap<T>方法把
把泛型类型赋予方法调用,就可以调用泛型方法:
但是,因为C#编译器会通过调用Swap方法来获取参数的类型,所以不需要把泛型类型赋予方法调用。
泛型方法可以像非泛型方法那样调用:
下面的例子使用泛型方法累加集合中的所有元素。
为了说明泛型方法的功能,下面的Account类包含name和balance:
应累加结余的所有账目操作都添加到List<Account>类型的账目列表中:
累加所有Account对象的传统方式是用foreach语句迭代所有的Account对象,如下所示。
foreach语句使用IEnumerable接口迭代集合的元素,所以AccumulateSimple()方法的参数是IEnumerable类型。
这样,AccumulateSimple()方法就可以用于所有实现IEnumerable接口的集合类。
Accumulate()方法的调用方式如下:
第一个实现代码的问题是,它只能用于Account对象。
使用泛型方法就可以避免这个问题。
Accumulate()方法的第二个版本接受实现了IAccount接口的任意类型。
如前面的泛型类所述,泛型类型可以用where子句来限制。
这个子句也可以用于泛型方法。
Accumulate()方法的参数改为IEnumerable<T>
Account
将Account
因为编译器会从方法的参数类型中自动推断出泛型类型参数,所以以如下方式调用Accumulate()方法是有效的:
Accumulate()方法将改为独立于任何接口。
9.6 泛型委托
如第7章所述,委托是类型安全的方法引用。
通过泛型委托,委托的参数可以在以后定义。
.NET Framework定义了一个泛型委托EventHandler,它的第二个参数是TEventArgs类型,所以不再
9.6.1 执行委托调用的方法
把Accumulate()方法改为有两个泛型类型。
TInput是要累加的对象类型,TSummary是返回类型。
Accumulate的第一个参数是IEnumerable<T>接口,这与以前相同。
第二个参数需要Action委托引用一个方法,来累加所有的结余。
Accumulate方法可以通过匿名方法调用,该匿名方法指定,账目的结余应累加到第二个Action 类型的参数中:
提示:
匿名方法和表达式参见第7章。
如果方法中:
联合使用
Action委托引用的方法可以实现任何逻辑。
例如,可以进行乘法操作,而不是加法操作。
Accumulate()方法和AccumulateIf()方法一起使用,会更灵活。
在AccumulateIf()中,使用了另一个Predicate<T>类型的参数。
Predicate<T>委托引用的方法会检查某个账目是否应累加进去。
在foreach 语句中,只有谓词match返回true,才会调用action方法:
调用 a = > a.Balance > 2000
9.6.2 对Array类使用泛型委托
第5章使用IComparable和IComparer接口,演示了Array类的几个排序技术。
从.NET 2.0开始,Array类的一些方法把泛型委托类型用作参数。
表9-2列出了这些方法、泛型类型和功能。
表9-2
Sort()
这样,就可以使用表达式传送两个Person对象,给数组排序。
对于Person对象数组,参数T是Person类型:
于是,就可以传送Console.WriteLine方法的地址,将每个人写入控制台。
WriteLine()方法的一个重载版本将Object类作为参数类型。
由于Person派生于Object,所以它适合于Person数组:
ForEach()
如果需要更多的控制,则可以传送一个表达式,其参数应匹配委托定义的参数:
下面是写入控制台的姓氏:
Array.FindAll()方法为数组中的每个元素调用谓词,并返回一个谓词是true的数组。
在这个例子中,对于Lastname以字符串"S"开头的所有Person对象,都返回true。
Array.ConvertAll()方法使用泛型委托Converter和两个泛型类型。
第一个泛型类型TInput是输入参数,第二个泛型类型TOutput是返回类型。
如果一种类型的数组应转换为另一种类型的数组,就可以使用ConvertAll()方法。
下面是一个与Person类无关的Racer类。
Person类有Firstname和Lastname属性,而Racer类为赛手的姓名定义了一个属性Name:
使用Array.ConvertAll(),很容易将persons数组转换为Racer数组。
给每个Person元素调用委托。
在每个Person元素的匿名方法的执行代码中,创建了一个新的Racer对象,将firstname和lastname
9.7 Framework的其他泛型类型
除了System.Collections.Generic命名空间之外,.NET Framework还有其他泛型类型。
这里讨论的结构和委托都位于System命名空间中,用于不同的目的。
本节讨论如下内容:
●结构Nullable<T>
●委托EventHandler<TEventArgs>
●结构ArraySegment<T>
9.7.1 结构Nullable<T>
数据库中的数字和编程语言中的数字有显著不同的特征,因为数据库中的数字可以为空,C#中的数字不能为空。
Int32是一个结构,而结构实现为值类型,所以它不能为空。
只有在数据库中,而且把XML数据映射为.NET类型,才不存在这个问题。
这种区别常常令人很头痛,映射数据也要多做许多工作。
一种解决方案是把数据库和XML文件中的数字映射为引用类型,因为引用类型可以为空值。
但这也会在运行期间带来额外的系统开销。
使用Nullable<T>结构很容易解决这个问题。
在下面的例子中,Nullable<T>用Nullable<int>实例化。
变量x现在可以像int那样使用了,进行赋值或使用运算符执行一些计算。
这是因为我们转换了Nullable<T>类型的运算符。
x还可以是空。
可以检查Nullable<T>的HasValue和Value属性,如果该属性有一个值,就可以访问该值:
因为可空类型使用得非常频繁,所以C#有一种特殊的语法,用于定义这种类型的变量。
定义这类变量时,不使用一般结构的语法,而使用?运算符。
在下面的例子中,x1和x2都是可空int类型的实例:
可空类型可以与null和数字比较,如上所示。
这里,x的值与null比较,如果x不是null,就与小于0的值比较:
可空类型还可以使用算术运算符。
变量x3是变量x1和x2的和。
如果这两个可空变量中有一个的值是null,它们的和就是null。
提示:
这里调用的GetNullableType()方法只是任意返回可空int的方法的占位符。
为了进行测试,可以把它实现为只返回null或返回任意整数值。
非可空类型可以转换为可空类型。
从非可空类型转换为可空类型时,在不需要强制类型转换的地
但从可空类型转换为非可空类型可能会失败。
如果可空类型的值是null,把null值赋予非可空类型,就会抛出InvalidOperationException类型的异常。
这就是进行显式转换时需要类型转换运算符的原因:
如果不进行显式类型转换,还可以使用接合运算符(coalescing operator)从可空类型转换为非可空类型。
接合运算符的语法是??,为转换定义了一个默认值,以防可空类型的值是null。
这里,如果x1是null,y1
9.7.2 EventHandler<TEventArgs>
在Windows Forms和Web应用程序中,为许多不同的事件处理程序定义了委托。
其中一些事件处理程序如下:
这些委托的共同点是,第一个参数总是sender,它是事件的起源,第二个参数是包含事件特定信息的类型。
使用新的EventHandler<TEventArgs>,就不需要为每个事件处理程序定义新委托了。
可以看出,第一个参数的定义方式与以前一样,但第二个参数是一个泛型类型TeventArgs。
where子句指定TEventArgs
9.8 小结
本章介绍了.NET 2.0中一个非常重要的特性:泛型。
通过泛型类可以创建独立于类型的类,泛型方法是独立于类型的方法。
接口、结构和委托也可以用泛型的方式创建。
泛型引入了一种新的编程方式。
我们介绍了算法(尤其是操作和谓词)如何用于不同的类,而且它们都是类型安全的。
泛型委托可以去除集合中的算法。
.NET Framework的其他类型包括Nullable<T>、EventHandler<TEventArgs>和ArraySegment<T>。
下一章利用泛型来介绍集合类。