《CLR via C#》 泛型

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

CLR允许创建泛型引用类型泛型值类型,但不允许创建泛型枚举类型。CLR还允许创建泛型接口泛型委托

类型实参(type argument):使用泛型类型或方法时指定的具体数据类型。

泛型的优势:

  • 源代码保护:开发人员不需访问算法的源代码,而C++模板则必须要将源码提供给开发人员。
  • 类型安全:保证只有与指定数据兼容的对象才能用于算法。
  • 更清晰的代码:减少了源代码中必须进行强制类型转换次数,使代码容易编写和维护。
  • 更佳的性能:值类型的实例能以传值方式传递,不需要装箱。由于不需要进行强制类型转换,所以CLR无需验证转型是否正确,提高效率。

提示

运算性能计时代码

 1sealed class OperationTimer : IDisposable {
 2    private Stopwatch _stopwatch;
 3    private String _text;
 4    private int _collectionCount;
 5
 6    public OperationTimer(String text) {
 7        PrepareForOperation();
 8        _text = text;
 9        _collectionCount = GC.CollectionCount(0);
10        // 此语句应是方法中的最后一句,从而保证计时准确性
11        _stopwatch = Stopwatch.StartNew();
12    }
13
14    public void Dispose() {
15        Console.WriteLine("{0} (GCs={1,3}) {2}", _stopwatch.Elapsed, GC.CollectionCount(0) - _collectionCount, _text);
16    }
17
18    private static void PrepareForOperation() {
19        GC.Collect();
20        GC.WaitForPendingFinalizers();
21        GC.Collect();
22    }
23}

开放类型和封闭类型

开放类型:具有泛型类型参数的类型称为开放类型。例如:List<>。CLR禁止构造开放类型的任何实例。

封闭类型:代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型,类型就成为封闭类型。例如:List<int>。CLR允许构造封闭类型的实例。

打印泛型类型,类型名以“’”字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。“[]”中是对应传递的数据类型,如果多个用逗号隔开。

1Console.WriteLine(typeof(List<>).ToString()); // 输出:System.Collections.Generic.List`1[T]
2Console.WriteLine(typeof(List<int>).ToString()); // 输出:System.Collections.Generic.List`1[System.Int32]

注意

  • CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。例如List<T>中定义的任何静态字段不会在List<int>List<String>之间共享。
  • 如果泛型类型定义了静态构造器,那么对每个封闭类型,这个构造器都只执行一次。
1class Test<T> {
2    static Test() {
3        Console.WriteLine("泛型静态构造器 " + typeof(T).  ToString());
4    }
5}
6Test<int> i1 = new Test<int>();        // 输出:泛型静态构造器 System.Int32
7Test<int> i2 = new Test<int>();
8Test<string> s1 = new Test<string>();  // 输出:泛型静态构造器 System.String
9Test<string> s2 = new Test<string>();

泛型类型和继承

泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。例如List<T>从Object派生,所以List<string>List<int>也从Object中派生。

创建一个泛型链表,并且其中每一个节点都可以是一种具体的类型(不能是Object),同时获得编译时的类型安全,并防止值类型装箱。

 1class Node {
 2    protected Node _next;
 3    public Node(Node next) {
 4        _next = next;
 5    }
 6}
 7
 8class TypeNode<T> : Node {
 9    public T _data;
10    public TypeNode(T data) : this(data, null) {
11    }
12    public TypeNode(T data, Node next) : base(next) {
13        _data = data;
14    }
15    public override String ToString() {
16        return _data.ToString() + ((_next != null) ? _next.ToString() : String.Empty);
17    }
18}
19
20Node head = new TypeNode<char>('.');
21head = new TypeNode<DateTime>(DateTime.Now, head);
22head = new TypeNode<String>("Today is ", head);
23Console.WriteLine(head.ToString());

代码爆炸

使用泛型类型的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。这样的缺点:CLR要为每种不同的方法/类型组合生成本机代码。这个现象为代码爆炸。可能造成引用程序的工作集显著增大,从而损害性能

CLR对这种情况的优化措施:

  • 为特性的类型实参调用一个方法,以后再用相同类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次。
  • CLR认为所有的引用类型实参都完全相同,所以代码能够共享。CLR之所以能执行这个优化,时因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象指针都以相同方式操纵。

注意

假如类型是值类型,CLR就必须专门为那个值类型生成本机代码。因为值类型字节大小不一定。即使值类型字节大小相同,CLR仍然无法共享代码,因为可能用不同的本机CPU指令来操纵这些值。

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

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

  • 不变量(invariant):意味着泛型类型参数不能更改。
  • 逆变量(contravariant):意味着泛型类型参数可以从一个类更改为它的某个派生类。在C#是用in关键字标记逆变量形式的泛型类型参数。逆变量泛型类型参数值出现在输入位置,比如作为方法的参数。
  • 协变量(covariant):意味着泛型类型参数可以从一个类更改为它的某个基类。C#用out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回值。
1// T用in关键字标记为逆变量
2// TResult用out关键字标记为协变量
3public delegate TResult Func<in T, out TResult>(T arg);
4
5// fn1变量引用一个方法,获取一个Object,返回一个ArgumentException
6Func<Object, ArgumentException> fn1 = null;
7// fn2变量引用一个方法,获取一个String,返回一个Exception
8// 因为Object是String的基类,Exception是ArgumentException的基类,所以fn1可转换为fn2。且不需要显示转换
9Func<String, Exception> fn2 = fn1; 

注意

  • 只有编辑器验证类型直接存在引用转换,这些可变性才有用。由于值类型需要装箱,所以值类型不具有这种可变性。
  • 对于泛型类型参数,如果要将该类型的实参传给使用out或ref关键字的方法,不允许使用可变性。

泛型方法和类型推断

C#编译器支持在调用泛型方法时进行类型推断。推断类型时,C#使用变量的数据类型,而不是变量引用的对象的实际类型。

 1public void Swap<T>(T a, T b) {
 2    T t = a;
 3    a = b;
 4    b = t;
 5}
 6
 7int n1 = 1, n2 = 2;
 8Swap(n1, n2);
 9String s1 = "s1";
10Object o1 = "o1";
11Swap(s1, o1);       // 错误,不能推断类型

如果调用同名的非泛型函数和泛型函数时。C#编辑器的策略是先考虑较明确的匹配,再考虑泛型匹配。

 1public static void Display(String s) {
 2    Console.WriteLine(s);
 3}
 4
 5public static void Display<T>(T o) {
 6    Console.WriteLine(o.ToString());
 7}
 8
 9Display("String1");         // 调用Display(String)
10Display(123);               // 调用Display<T>(T)
11Display<String>("String2"); // 调用Display<T>(T)

可验证性和约束

约束的作用是限制指定成泛型实参的类型数量。编译器负责保证类型实参符合指定约束。

1// where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口
2public static T Min<T>(T o1, T o2) where T : IComparable<T> {
3    return o1.CompareTo(o2) < 0 ? o1 : o2;
4}

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

1class A {}
2class A<T> {}
3class A<T1, T2> {}
4
5class A<T> where T : class {} // 错误:与A<T>冲突

重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指示的约束。且并不允许为重写方法的类型参数指定任何约束。但类型参数的名称时可以改变的。类似地,实现接口方法时,方法必须指定与接口方法等量的类型参数,这些类型参数将继承由接口方法指定的约束。

1class Base {
2    public virtual void M<T1, T2>() : where T1 : struct where T2 : class {}
3}
4
5class Derived : Base {
6    // 不允许指定约束
7    public override void M<T3, T4>() {}
8}

主要约束

类型参数可以指定零个或一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.ObjectSystem.ArraySystem.DelegateSystem.MulticastDelegateSystem.ValueTypeSystem.EnumSystem.Void

指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么与约束类型相同,要么从约束类型派生

如果类型参数没有指定主要约束,就默认为System.Object

两个特殊的主要约束:classstruct

class约束向编译器承诺类型实参是引用类型。任何类、接口、委托或数组都满足这个约束。

struct约束向编译器承诺类型实参是值类型。但CLR将System.Nullable<T>值类型视为特殊,不满足struct约束

次要约束

类型参数可以指定零个或多个次要约束。次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。

还有一种次要约束称为类型实参约束或裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么是约束的类型的派生类。一个类型参数可以指定零个或多个类型参数约束。

1// T与TBase相同或从TBase派生
2private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase {}

构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。如果构造器约束和struct约束一起使用是错误的,因为struct必有无参构造器。

1class A<T> where T : new() {
2    public static T Create() {
3        // 如果T为引用类型,new()约束保证其必须有公共无参构造函数。
4        // 如果T为值类型,其本身一定有公共无参构造函数。
5        return new T();
6    }
7}

泛型使用的一些注意点

泛型类型变量的转型

将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型。

1private static void Func<T>(T obj) {
2    int x = (int)obj; // 错误
3    int x1 = (int)(Object)obj; // 可通过编译,但运行时如果T不为int会抛出异常
4}

将泛型类型变量设为默认值

泛型可以用default关键字来设置默认值。JIT编译器执行时,如果T是引用类型,就设置为null;如果是值类型,就设置为0;

1private static void Func<T>() {
2    T temp = default(T);
3}

两个泛型互相比较

如果泛型不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的

基元值类型可以比较,但非基元值类型C#编译器不知道如果比较,所以约束为struct的比较是非法的。

不允许将类型参数约束成具体的值类型,因为值类型隐式密封,不可能存在从值类型派生的类型。

泛型类型变量作为操作数使用

不能将基元类型的操作符(比如+,-,*,/等)引用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量引用任何操作符。