泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
简单来说,开发人员先定义好算法,必然排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。
泛型为开发人员提供了以下优势:
- 源代码保护:使用泛型算法的开发人员不需要访问算法的源代码。然后,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
- 类型安全:将泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能用于算法。
- 更清晰的代码:由于编译器强制类型安全性,所以减少了源代码中必须进行的强制类型转换次数,使代码更容易编写和维护。
- 更佳的性能:没有泛型的时候,要想定义常规化的算法,它的所有成员都要定义成操作Object数据类型。要用这个算法来操作值类型的实例,CLR必须在调用算法的成员之前对值类型实例进行装箱。
为了理解性能优化,我们可以通过如下的代码进行测试:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;class Program
{static void Main(string[] args){ValueTypePerfTest();ReferenceTypePerfTest();}private static void ValueTypePerfTest(){const int count = 100000000;using(new OperationTimer("List<int>")){List<int> l = new List<int>();for (int i = 0; i < count; i++){l.Add(i);int x = l[i];}l = null;}using (new OperationTimer("ArrayList of int")){ArrayList l = new ArrayList();for (int i = 0; i < count; i++){l.Add(i); // 装箱int x = (int)l[i]; // 拆箱}l = null;}}private static void ReferenceTypePerfTest(){const int count = 100000000;using (new OperationTimer("List<string>")){List<string> l = new List<string>();for (int i = 0; i < count; i++){l.Add("X");string x = l[i];}l = null;}using (new OperationTimer("ArrayList of string")){ArrayList l = new ArrayList();for (int i = 0; i < count; i++){l.Add("X"); string x = (string)l[i]; }l = null;}}
}internal sealed class OperationTimer : IDisposable
{private Stopwatch stopwatch;private string text;private int collectionCollect;public OperationTimer(string text){PrepareForOperation();this.text = text;collectionCollect = GC.CollectionCount(0);stopwatch = Stopwatch.StartNew();}public void Dispose(){Console.WriteLine($"{stopwatch.Elapsed} (GCs={GC.CollectionCount(0) - collectionCollect}) {text}");}private static void PrepareForOperation(){GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();}
}
最后会得到如下输出:
很明显,在操作值类型时,泛型算法比非泛型算法快了近乎11倍了。此外用ArrayList操作值类型,会造成大量装箱,最终要进行293次垃圾回收。
不过,引用类型,差异则没有那么明显了,GC一样都是0,时间虽然泛型略快一点,但也不像值类型有这么大的差距。
开放类型和封闭类型
具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型,值类型,接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放开放类型的任何实例。
代码引用泛型类型时可指定一组泛型类型参数。为所有类型参数都传递了实际的数据类型,类型就称为封闭类型。CLR允许构造封闭类型的实例。例如以下例子中,我分别尝试用反射的方法去实例化一个开放类型和封闭类型:
using System;
using System.Collections.Generic;class Program
{static void Main(string[] args){try{Type t1 = typeof(Dictionary<,>);var o1 = Activator.CreateInstance(t1);Console.WriteLine($"{t1.ToString()}实例化传功");}catch(ArgumentException e){Console.WriteLine(e);}try{Type t2 = typeof(Dictionary<int, int>);var o2 = Activator.CreateInstance(t2);Console.WriteLine($"{t2.ToString()}实例化传功");}catch(ArgumentException e){Console.WriteLine(e);}}
}--------输出结果------
ystem.ArgumentException: Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.at System.RuntimeType.CreateInstanceCheckThis()at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)at System.Activator.CreateInstance(Type type)at Program.Main(String[] args) in C:\Users\LH89\source\repos\ConsoleApp3\Program.cs:line 13
System.Collections.Generic.Dictionary`2[System.Int32,System.Int32]实例化传功
可以看出,只有封闭类型才能实例化成功。从输出类型可以看出,类型名以“`”字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。
还要注意,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,则List<int>和List<string>不会共享这些静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。
泛型类型和继承
泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义了一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。
class Program
{static void Main(string[] args){TypeNode<int> a = new TypeNode<int>();TypeNode<string> b = new TypeNode<string>();Node start = new Node();start.next = a;start.next.next = b;}
}public class Node
{public Node next;
}public class TypeNode<T> : Node { }
例如上例中,Node<int>和Node<string>都继承Node基类,我们也可以利用到多态的特点,将值类型和引用类型装入同一个链表中,同时避免了值类型装箱拆箱的特点。
同一性
为了对语法进行增强,有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有类型实参,例如:
public class DateTimeList : List<DateTime> { }
此时就可以简化创造列表代码:
List<DateTime> list1 = new List<DateTime>(); ->
DateTimeList list2 = new DateTimeList();
这样做表面上是简化了代码书写,但其实不妥!绝对不要出于增强源码可读性的目的来定义一个新类。这样会散失同一性(identity)和相等性(equivalence),例如我们此时比较 list1.GetType() == list2.GetType(),会返回一个false,因为比较的是不同类型的两个对象。这也意味着如果方法的原型接受一个DateTimeList,我们无法把List<DateTime>传给他,这会导致开发非常混乱。
C#也考虑到了泛型的书写困难,所以他提供了简化的语法来引用泛型封闭类型,例如我们可以在源文件顶部这样声明,就不会丧失同一性也能保证代码可读性:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
代码爆炸
使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。这样有个缺点:CLR要为每种不同的方法/类型组合生成本机代码,这种现象称为代码爆炸。它会使得应用程序的工作集显著增大,从而损害性能。
幸好,CLR采用了一些优化措施缓解了代码爆炸:
- 假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
- CLR认为所有引用类型实参都完全相同,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。CLR之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象指针都以相同的方式操纵。
但如果类型实参是值类型,CLR就必须专门为那个值类型生成本机代码。这是因为值类型的大小不定。即使2个值类型大小一样(比如int32和uint32,都是32位),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。
泛型接口
没有泛型接口,每次用非泛型接口(入IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR提供了对泛型接口的支持,例如:
public interface IAnimal<T>
{T animal { get; }
}public class Dog : IAnimal<Dog>
{public Dog animal => new Dog();
}public class Number : IAnimal<int>
{public int animal => 0;
}
泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
具体例子先暂时跳过,看完17章泛型再来补充
泛型方法
泛型方法的存在,为开发人员提供了极大的灵活性。例如:
private void Swap<T>(ref T o1, ref T o2){T temp = o1;o1 = o2;o2 = temp;}
有一点要注意的是,作为out/ref实参传递的变量必须具有与方法参数相同的类型,以防止损坏类型安全性。
可验证性和约束
约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型指向更多操作。例如:
public static T Min<T>(T o1, T o2) where T : IComparable<T>{if(o1.CompareTo(o2) < 0){return o1;}return o2;}
C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。
约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。
重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法指定的约束上,事实上,根本不允许为重写的方法的类型参数指定任何约束。单类型参数的名称是可以改变的。
主要约束
类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。
指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如:
public static T Min<T>(T o1, T o2) where T : List<int>{return o1.Count < o2.Count ? o1 : o2;}
有两个特殊的主要约束:class和struct。
class约束:向编译器承诺类型实参是引用类型。(任何类类型、接口类型、委托类型或者数组类型都满足这个约束)
struct约束:向编译器承诺类型实参是值类型。(包括枚举在内的任何值类型都满足这个约束,但编译器和CLR将任何System.Nullable<T>值类型视为特殊类型,不满足这个struct约束)
原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。
次要约束
类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺,类型实参实现了接口,由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。
还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么是约束的类型,要么是约束的类型的派生类。例如:
private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase{List<TBase> baseList = new List<TBase>(list.Count);foreach(var item in list){baseList.Add(item);}return baseList;}
构造器约束
类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。例如:
private class ConstructorConstaint<T> where T : new() {public static T Factory(){return new T();}}