《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.Object
,System.Array
,System.Delegate
,System.MulticastDelegate
,System.ValueType
,System.Enum
或System.Void
。
指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么与约束类型相同,要么从约束类型派生。
如果类型参数没有指定主要约束,就默认为System.Object
。
两个特殊的主要约束:class
和struct
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的比较是非法的。
不允许将类型参数约束成具体的值类型,因为值类型隐式密封,不可能存在从值类型派生的类型。
泛型类型变量作为操作数使用
不能将基元类型的操作符(比如+,-,*,/等)引用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量引用任何操作符。