定义:
所谓泛型,即通过参数化类型实现在同一份代码上操作多种类型的数据,泛型编程是一种范式的转化(在这里体现为类型的晚绑定),他利用参数化类型,将类型抽象化,从而实现代码的灵活复用,精简代码。
泛型的好处:
a.减少了对对象进行装箱和拆箱所导致的性能成本,提高了效率。
b.赋予了代码更强的类型安全。
c.实现了更为灵活的代码复用。
注:1.NET参数化类型不是编译(JIT编译)时被实例化,而是运行时被实例化。
2.由微软在产品文档中提出建议,所有的泛型参数名称都以T开头,这是作为一种编码的通用规范。
在定义泛型时,可以对客户端代码在实例化类时用于类型参数的类型施加一些限制,如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译错误,这些限制称为约束,约束是使用where关键字实现的。
每个泛型参数至少拥有一个主约束,泛型的主约束是指指定泛型参数必须是或者继承自某个引用类型。每个泛型参数可以具有多个次约束,次约束和主约束的语法基本相同,但它规定的是某个泛型参数必须实现所有次约束指定的接口。
下面列出了五种类型的约束:
T:struct 类型参数必须为值类型,可以指定除 Nullable 以外的任何值类型。
T:class 类型参数必须为引用类型,包括类、接口、委托、和数组。
T:new() 类型参数必须具有无参公共构造函数,当与其他约束一起使用时,new() 约束必须最后指定。
T:<基类名> 类型参数必须为指定的基类或继承自该基类的子类。
T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。
T:U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束.
泛型的编译和运行机制
C#泛型能力由CLR在运行时支持,区别于C++编译时模板的机制,和java编译时的“搽拭法”,这事得泛型能力可以在各个支持CLR语言之间进行无缝的互操作。C#泛型代码在编译为IL代码和元数据时,采用特殊的占位符来表示泛型实例,并用专有的IL指令来支持泛型操作,而真正的泛型实例化工作发生在JIT编译时。
这里我们对三种语言对泛型的支持做一个对比:
C++的模板机制:C++在编译时会根据每一个传入的类型参数创建一份基于特定类型的类型码,因此如果有多个类型参数传入,编译时就会生成多份相似的类型码,容易导致代码膨胀。因此C++的泛型只是实现了在源代码层面的复用,并没有实现IL层面的代码复用。
C#:第一轮编译时,编译器只会为泛型类型产生一个“泛型版”的IL代码和元数据,并不进行泛型类型的实例化,T在中间只是充当占位符,这一点可以通过下面展示的一份泛型的IL代码来证明。只有JIT编译时CLR才会针对不同的类型产生不同的类型码,在类型码产生的过程中,CLR进行了许多的优化:1.CLR为所有引用类型的类型参数产生一份共同的一份类型码,所有的引用类型共用这一份类型码。2.对于每一个不同的值类型,CLR将为其产生一份独立的类型码。另外,C#泛型类型携带有丰富的元数据,因此C#的泛型类型可以应用于强大的反射技术。
java:java在进行第一阶段的编译时,将泛型类型用Object类型进行替换,因此类型参数实例化的时候需要进行大量的装箱和拆箱工作,但是这些工作并不需要我们去做,编译器会自动的进行这些工作,性能成本比较高。本质上讲并没有实现真正意义上的泛型,是编译器的一种欺骗行为。
下面通过具体的例子来一点一点的深入泛型。
我们来实现一个最简单的冒泡排序(Bubble Sort)算法,如果你没有使用泛型的经验,我猜测你可能会毫不犹豫地写出下面的代码来,因为这是大学教程的标准实现:
public class Sort{
public void BubbleSort(int[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
后来我们需要对一个byte类型的数组进行排序,而上面排序的方法只能对int型的数组进行排序,因此我们不得不重写代码:
public class Sort{
public void BubbleSort(byte[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
byte temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
现在我们将int[]和byte[]用占位符来替代,形成一种通用的代码:
public class Sort{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
但是我们又发现了一个问题:当我们定义一个类,而这个类需要引用它本身以外的其他类型时,如何将这个类型参数传进来了,此时就需要使用一种特殊的语法来传递这个T占位符,我们在类名称的后面加了一个尖括号,使用这个尖括号来传递我们的占位符,也就是类型参数。
public class Sort<T>{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
使用的时候我们就可以这样使用:
public class Test{
public static void Main(){
Sort<int> sorter = new Sort<int>();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
}
}
上面所讲述的一切都是一个泛型的典型应用,可以看到,通过使用泛型,我们极大地减少了重复代码,使我们的程序更加清爽,泛型类就类似于一个模板,可以在需要时为这个模板传入任何我们需要的类型。
下面我们来谈一下泛型约束。
实际上,如果你运行一下上面的代码,发现他们无法通过编译,为什么了,就是因为有了T的存在,T是晚绑定的,因此在编译时编译器无法得知T的实例是采用什么样的标准来进行大小的比较的,下面我们举例说明:
假如我们有一个自定义的类Book,它定义了书,它包含两个私有字段_id和_title,两个外部属性:ID和Title,以及两个构造器
public class Book
{
private int _id;
private string _title;
public Book(){ }
public Book(int id,string title)
{
this._id=id;
this._title=title
}
public int ID
{
get{return _id;}
set{_id=value;}
}
public string Title
{
get{return _title;}
set{_title=value;}
}
}
现在我们创建一个Book型的数组,然后用Sort类中的方法对其进行排序:
class Test{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort=new Sort<Book>;
Sort.BubbleSort(bookArray);
foreach (Book b in bookArray) {
Console.WriteLine("Id:{0}", b.Id);
Console.WriteLine("Title:{0}\n", b.Title);
}
}
}
可能你觉得这样很好,基本没什么问题,但是我们来看看BubbleSort()方法的实现,我截取关键的一段:
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
大家会看到if语句里面会对数组里面的两个元素进行比较,那么问题就出在这儿,以前当类型为int型的时候,我们直接这样进行比较,无可厚非,但是现在不同了,我们的类型是Book,那么我要问,book1和book2到底谁大了,有的人说book1大,有的人说book2大,这里就涉及到一个判断依据的问题。那么如何来实现这种比较了,答案是:让需要进行比较的类实现IComparable接口。也就是说只有实现了IComparable接口的类型才能作为类型参数被传入,即我们需要对传入参数的类型进行一些约束,这就是我们要讲的泛型约束,在本例中我们实现的是接口约束。
接下来我们就让Book类来实现IComparable接口,即在类的内部定义一个比较的标准,我们这里采用的标准是比较ID:
public class Book : IComparable
{
public int CompareTo(object obj) //实现接口
{
Book book2=(Book)obj;
return this.ID.CompareTo(book2.ID);
}
private int _id;
private string _title;
public Book(){ }
public Book(int id,string title)
{
this._id=id;
this._title=title;
}
public int ID
{
get{return _id;}
set{_id=value;}
}
public string Title
{
get{return _title;}
set{_title=value;}
}
}
现在我们应该可以进行比较了吧,还不行,因为Sort类是一个泛型类,JIT编译时编译器对于传入该类的类型参数一无所知(类型的晚绑定),明确的说需要等到运行时才能确定参数,也不会做任何猜想,虽然我们知道Book类实现了
IComparable接口,但编译器并不知道,因此我们必须Sort<T>类(即告诉JIT编译器),它所接受的类型参数必须实现了IComparable接口,这便是泛型约束,下面我们来对泛型类Sort<T>的传入参数进行约束,同时我们对比较大小的方法进行一些修改。
public class Sort<T> where T : IComparable
{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j].CompareTo(array[j-1])<0) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
此时我们再次运行下面定义的代码
class Test{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort=new Sort<Book>();
Sort.BubbleSort(bookArray);
foreach (Book b in bookArray) {
Console.WriteLine("Id:{0}", b.ID);
Console.WriteLine("Title:{0}\n", b.Title);
}
Console.ReadLine();
}
}
会得到结果:
ID:1
Title:guowenhui
ID:2
Title:dongyaguang
下面是完整的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class Book : IComparable
{
public int CompareTo(object obj) //实现接口
{
Book book2 = (Book)obj;
return this.ID.CompareTo(book2.ID);
}
private int _id;
private string _title;
public Book() { }
public Book(int id, string title)
{
this._id=id;
this._title = title;
}
public int ID
{
get { return _id; }
set { _id = value; }
}
public string Title
{
get { return _title; }
set { _title = value; }
}
}
public class Sort<T> where T : IComparable
{
public void BubbleSort(T[] array)
{
for (int i = 0; i <= array.Length - 2; i++)
{
for (int j = array.Length - 1; j >= 1; j--)
{
if (array[j].CompareTo(array[j - 1]) < 0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
class Test
{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort = new Sort<Book>();
sort.BubbleSort(bookArray);
foreach (Book b in bookArray)
{
Console.WriteLine("Id:{0}", b.ID);
Console.WriteLine("Title:{0}\n", b.Title);
}
Console.ReadLine();
}
}
}
泛型接口
没有泛型接口,每次试图使用一个非泛型接口(如IComparable)来操纵一个值类型时,都会进行装箱,而且会丢失编译时的类型安全性。这会严重限制泛型类型的应用。所以,CLR提供了对泛型接口的支持。一个引用类型或值类型为了实现一个泛型接口,可以具体指定类型实参;另外,一个类型也可以保持类型实参的未指定状态来实现一个泛型接口。来看一些例子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ConsoleApplication1
{
interface ITest<T, V> //泛型接口的定义
where T : class
where V : struct
{
void Print(T t, V v);
}
class TestA //自定义引用类型
{
private string _name;
public string Name
{
set { _name = value; }
get { return _name; }
}
}
struct TestB //自定义值类型
{
private int _age;
public int Age
{
get { return _age; }
set { _age = value; }
}
}
class TestC : ITest<TestA,TestB> //继承并实现接口
{
public void Print(TestA A, TestB B)
{
Console.WriteLine(A.Name + " is " + B.Age);
}
}
class Program
{
static void Main()
{
TestA testA = new TestA();
testA.Name = "guowenhui";
TestB testB = new TestB();
testB.Age = 21;
TestC testC = new TestC();
testC.Print(testA, testB);
Console.ReadLine();
}
}
}