第 12 章 泛型

第 12 章 泛型

本章内容

泛型(generic)是 CLR 和编程语言的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。

CLR 允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,CLR 还允许创建泛型接口和泛型委托。 CLR 允许在引用类型、值类型或接口中定义泛型方法。

从以上代码可看出泛型为开发人员提供了以下优势。

  • 源代码保护

  • 类型安全

  • 更清晰的代码

  • 更佳的性能

注意 应该意识到,首次为特定数据类型调用方法时,CLR 都会为这个方法生成本机代码。这会增大应用程序的工作集(working set)大小,从而损害性能。

12.1 FCL 中的泛型

12.2 泛型基础结构

12.2.1 开发类型和封闭类型

具有泛型类型参数的类型仍然是类型,CLR 同样会为它创建内部的类型对象。具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。

为所有类型参数都传递了实际的数据类型,类型就成为封闭类型。CLR 允许构造封闭类型的实例。然而,代码引用泛型类型的时候,可能留下一些泛型类型实参未指定。这会在 CLR 中创建新的开放类型对象,而且不能创建该类型的实例。

类型名以'字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。

每个封闭类型对象都有自己的静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。例如,我们可以像下面这样定义只能处理么枚举类型的泛型类型:

1
2
3
4
5
6
7
internal sealed class GenericTypeThatRequiresAnEnum<T> {
static GenericTypeThatRequiresAnEnum() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enumerated type");
}
}
}

12.2.3 泛型类型同一性

一些开发人员可能首先定义下面这样的类:

1
2
3
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
2
3
4
5
6
public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {
public CallMe(Object object, IntPtr method);
public virtual TReturn Invoke(TKey key, TValue value);
public virtual IAsyncResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object);
public virtual TReturn EndInvoke(IAsyncResult result);
}

12.5 委托和接口的逆变和协变泛型类型实参

委托的每个泛型类型参数都可标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是以下任何一种形式。

  • 不变量(invariant) 意味着泛型类型参数不能更改。到目前为止,你在本章看到的全是不变量形式的泛型类型参数。

  • 逆变量(contravariant) 意味着泛型类型参数可以从一个类更改为它的某个派生类。在 C# 是用 in 关键字标记逆变量形式的泛型类型参数。协变量泛型类型参数只出现在输入位置,比如作为方法的参数。

  • 协变量(covariant) 意味着泛型类型参数可以从一个类更改为它的某个基类。C#是用out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型。

12.6 泛型方法

类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。然而,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为、返回值或局部变量的类型使用。

泛型方法和类型推断

编译器会在调用泛型方法时自动判断(或者说推断)要使用的类型。

12.7 泛型和其他成员

在C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。

12.8 可验证性和约束

约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。以下是新版本的Min方法,它指定了一个约束(加粗显示):

1
2
3
4
public static T Min<T>(T ol, T o2) where T : IComparable<T> {
if (o1.CompareTo(o2) < 0) return o1;
return o2;
}

CLR 不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 可定义以下类型
internal sealed class AType { }
internal sealed class AType<T> { }
internal sealed class AType<T1, T2> { }

// 错误:与没有约束的 AType<T> 冲突
internal sealed class AType<T> where T : IComparable<T> { }

// 错误:与 AType<T1, T2>冲突
internal sealed class AType<T3, T4> { }

internal sealed class AnotherType {
// 可定义以下方法,参数个数不同:
private static void M() { }
private static void M<T>() { }
private static void M<T1, T2>() { }

// 错误:与没有约束的 M<T> 冲突
private static void M<T>() where T : IComparable<T> { }

// 错误:与 M<T1, T2>冲突
private static void M<T3, T4>() { }
}

重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。事实上,根本不允许为重写方法的类型参数指定任何约束。但类型参数的名称是可以改变的。

12.8.1 主要约束

类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。

有两个特殊的主要约束:classstruct。其中,class约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。

12.8.2 次要约束

类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。

还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么是约束的类型的派生类。一个类型参数可以指定零个或者多个类型参数约束。下面这个泛型方法演示了如何使用类型参数约束:

1
2
3
4
5
6
7
private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase {
List<TBase> baseList = new List<TBase>(list.Count);
for (Int32 index = 0; index < list.Count; index++) {
baseList.Add(list[index]);
}
return baseList;
}

12.8.3 构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。
目前,CLR(以及C# 编译器)只支持无参构造器。

12.8.4 其他可验证性问题

  1. 泛型类型变量的转型
    将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型:
1
2
3
4
private static void CastingAGenericTypeVariablel<T>(T obj) {
Int32 x = (Int32) obj; // 错误
String s = (String) obj; // 错误
}
1
2
3
4
5
6
7
上述两行会造成编译器报错,因为 `T` 可能是任意类型,无法保证成功转型。为了修改上述代码使其能通过编译,可以先转型为`Object`:  

```C#
private static void CastingAGenericTypeVariable2<T>(T obj) {
Int32 x = (Int32) (Object) obj; // 无错误
String s = (String) (Object) obj; // 无错误
}

虽然代码现在能编译,但 CLR 仍有可能在运行时抛出 InvalidCastException 异常。
2. 将泛型类型变量设为默认值
将泛型类型变量设为 null 是非法的,除非将泛型类型约束成引用类型。

1
2
3
4
private static void SettingAGenericTypeVariableToNull<T>() {
T temp = null; // error CS0403 - 无法将 null 转换为类型参数 “T”,
// 因为它可能是不可以为 null 的值类型。请考虑改用 default(T)
}

由于未对 T 进行约束,所以它可能是值类型,而将值类型的变量设为null是不可能的。如果T被约束成引用类型,将temp设为null就是合法的,代码能顺利编译并运行。

Microsoft 的 C# 团队认为有必要允许开发人员将变量设为它的默认值,并专门为此提供了 default 关键字:

1
2
3
private static void SettingAGenericTypeVariableToDefaultValue<T>() {
T temp = default(T); // 正确
}

以上代码中的 default 关键字告诉 C# 编译器和 CLR 的 JIT 编译器,如果 T 是引用类型,就将temp 设为 null;如果是值类型,就将 temp 的所有位设为 0

  1. 将泛型类型变量与null进行比较
    无论泛型类型是否被约束,使用 ==!= 操作符将泛型类型变量与 null 进行比较都是合法的:
1
2
3
private static void ComparingAGenericTypeVariableWithNull<T>(T obj) {
if (obj == null) { /* 对于值类型,永远都不会执行 */ }
}
  1. 两个泛型类型变量相互比较
    如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的:
1
2
3
private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) {
if (o1 == o2) { } // 错误
}
  1. 泛型类型变量作为操作数使用
    最后要注意,将操作符应用于泛型类型的操作数会出现大量问题。不能将这些擦作符应用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量应用任何操作符。