第 12 章 泛型
第 12 章 泛型
NyxX第 12 章 泛型
本章内容
泛型(generic)是 CLR 和编程语言的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
CLR 允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,CLR 还允许创建泛型接口和泛型委托。 CLR 允许在引用类型、值类型或接口中定义泛型方法。
从以上代码可看出泛型为开发人员提供了以下优势。
源代码保护
类型安全
更清晰的代码
更佳的性能
注意 应该意识到,首次为特定数据类型调用方法时,CLR 都会为这个方法生成本机代码。这会增大应用程序的工作集(working set)大小,从而损害性能。
12.1 FCL 中的泛型
12.2 泛型基础结构
12.2.1 开发类型和封闭类型
具有泛型类型参数的类型仍然是类型,CLR 同样会为它创建内部的类型对象。具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。
为所有类型参数都传递了实际的数据类型,类型就成为封闭类型。CLR 允许构造封闭类型的实例。然而,代码引用泛型类型的时候,可能留下一些泛型类型实参未指定。这会在 CLR 中创建新的开放类型对象,而且不能创建该类型的实例。
类型名以'字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。
每个封闭类型对象都有自己的静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。例如,我们可以像下面这样定义只能处理么枚举类型的泛型类型:
1 | internal sealed class GenericTypeThatRequiresAnEnum<T> { |
12.2.3 泛型类型同一性
一些开发人员可能首先定义下面这样的类:
1 | internal sealed class DateTimeList : List<DateTime> { |
绝对不要单纯出于增强源码可读性的目的来定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence),如以下代码所示:
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
上述代码运行时,sameType会被初始化为false,因为比较的是两个不同类型的对象。
12.2.4 代码爆炸
代码爆炸是指使用泛型类型参数的方法在编译时生成本机代码,会导致应用程序工作集增大,影响性能。CLR提供了优化措施,例如相同类型实参只编译一次代码,引用类型实参能共享代码,但值类型实参需要生成专门的本机代码。
12.3 泛型接口
泛型接口的支持对CLR来说也很重要。没有泛型接口,每次用非泛型接口(如 IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。CLR 提供了对泛型接口的支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。
12.4 泛型委托
例如,假定像下面这样定义泛型委托:
public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);
编译器会将它转换成如下所示的类:
1 | public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate { |
12.5 委托和接口的逆变和协变泛型类型实参
委托的每个泛型类型参数都可标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是以下任何一种形式。
不变量(invariant) 意味着泛型类型参数不能更改。到目前为止,你在本章看到的全是不变量形式的泛型类型参数。
逆变量(contravariant) 意味着泛型类型参数可以从一个类更改为它的某个派生类。在 C# 是用
in关键字标记逆变量形式的泛型类型参数。协变量泛型类型参数只出现在输入位置,比如作为方法的参数。协变量(covariant) 意味着泛型类型参数可以从一个类更改为它的某个基类。C#是用
out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型。
12.6 泛型方法
类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。然而,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为、返回值或局部变量的类型使用。
泛型方法和类型推断
编译器会在调用泛型方法时自动判断(或者说推断)要使用的类型。
12.7 泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。
12.8 可验证性和约束
约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。以下是新版本的Min方法,它指定了一个约束(加粗显示):
1 | public static T Min<T>(T ol, T o2) where T : IComparable<T> { |
CLR 不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。
1 | // 可定义以下类型 |
重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。事实上,根本不允许为重写方法的类型参数指定任何约束。但类型参数的名称是可以改变的。
12.8.1 主要约束
类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。
有两个特殊的主要约束:class和struct。其中,class约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。
12.8.2 次要约束
类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。
还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么是约束的类型的派生类。一个类型参数可以指定零个或者多个类型参数约束。下面这个泛型方法演示了如何使用类型参数约束:
1 | private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase { |
12.8.3 构造器约束
类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。
目前,CLR(以及C# 编译器)只支持无参构造器。
12.8.4 其他可验证性问题
- 泛型类型变量的转型
将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型:
1 | private static void CastingAGenericTypeVariablel<T>(T obj) { |
1 | 上述两行会造成编译器报错,因为 `T` 可能是任意类型,无法保证成功转型。为了修改上述代码使其能通过编译,可以先转型为`Object`: |
虽然代码现在能编译,但 CLR 仍有可能在运行时抛出 InvalidCastException 异常。
2. 将泛型类型变量设为默认值
将泛型类型变量设为 null 是非法的,除非将泛型类型约束成引用类型。
1 | private static void SettingAGenericTypeVariableToNull<T>() { |
由于未对 T 进行约束,所以它可能是值类型,而将值类型的变量设为null是不可能的。如果T被约束成引用类型,将temp设为null就是合法的,代码能顺利编译并运行。
Microsoft 的 C# 团队认为有必要允许开发人员将变量设为它的默认值,并专门为此提供了 default 关键字:
1 | private static void SettingAGenericTypeVariableToDefaultValue<T>() { |
以上代码中的 default 关键字告诉 C# 编译器和 CLR 的 JIT 编译器,如果 T 是引用类型,就将temp 设为 null;如果是值类型,就将 temp 的所有位设为 0。
- 将泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将泛型类型变量与null进行比较都是合法的:
1 | private static void ComparingAGenericTypeVariableWithNull<T>(T obj) { |
- 两个泛型类型变量相互比较
如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的:
1 | private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) { |
- 泛型类型变量作为操作数使用
最后要注意,将操作符应用于泛型类型的操作数会出现大量问题。不能将这些擦作符应用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量应用任何操作符。





