泛型的英文解释为generic,固然咱们查询这个单词时,更多的解释是通用的意思,然而有些人会认为明明是通用类型,怎么成泛型了的,其实这二者并不冲突的,泛型原本表明的就是通用类型,只是微软可能有一个比较官方的词来形容本身引入的特性而已,既然泛型是通用的, 那么泛型类型就是通用类型的,即泛型就是一中模子。 在生活中,咱们常常会看到模子,像咱们日常生活中用的桶子就是一个模子,咱们能够用桶子装水,也能够用来装油,牛奶等等,然而把这些都装进桶子里面以后,它们都会具备桶的形状(水,牛奶和油原本是没有形的),即具备模子的特征。一样,泛型也是像桶子同样的模子,咱们能够用int类型,string类型,类去实例化泛型,实例化以后int,string类型都会具备泛型类型的特征(就是说可使用泛型类型中定义的方法,如List<T>泛型,若是用int去初始化它后,List<int>的实例就能够用List<T>泛型中定义的全部方法,用string去初始化它也同样,和咱们生活中的用桶装水,牛奶,油等很是相似
html
你们经过第一部分知道了什么是泛型,然而C#2.0中为何要引入泛型的?这答案固然是泛型有不少好处的。下面经过一个例子来讲明C# 2.0中为何要引入泛型,而后再介绍下泛型所带来的好处有哪些。算法
当咱们要写一个比较两个整数大小的方法时,咱们可能很快会写出下面的代码:编程
public class Compare { // 返回两个整数中大的那一项 public static int Compareint(int int1, int int2) { if (int1.CompareTo(int2) > 0) { return int1; } return int2; } }
然而需求改变为又要实现比较两个字符串的大小的方法时,咱们又不得不在类中实现一个比较字符串的方法:数组
若是需求又改成要实现比较两个对象之间的大小时,这时候咱们又得实现比较两个对象大小的方法,然而咱们中需求中能够看出,需求中只是比较的类型不同的,其实现方式是彻底同样的,这时候咱们就想有没有一种类型是通用的,咱们能够把任何类型当作参数传入到这个类型中去实例化为具体类型的比较,正是有了这个想法,同时微软在C#2.0中也想到了这个问题,因此就致使了C#2.0中添加了泛型这个新的特性,泛型就是——通用类型,有了泛型以后就能够很好的帮助咱们刚才遇到的问题的,这样就解决了咱们的第一个疑问——为何要引入泛型。下面是泛型的实现方法:安全
public class Compare<T> where T : IComparable { public static T CompareGeneric(T t1, T t2) { if (t1.CompareTo(t2) > 0) { return t1; } else { return t2; } } }
这样咱们就不须要针对每一个类型实现一个比较方法,咱们能够经过下面的方式在主函数中进行调用的:函数
public class Program { static void Main(string[] args) { Console.WriteLine(Compare<int>.CompareGeneric(3, 4)); Console.WriteLine(Compare<string>.CompareGeneric("abc", "a")); Console.Read(); } }
经过上面的代码你们确定能够理解C# 2.0中为何要引入泛型的,然而泛型能够给咱们带什么好处的呢?从上面的例子能够看出,泛型能够帮助咱们实现代码的重用,你们很清楚——面向对象中的继承也能够实现代码的重用,然而泛型提供的代码的重用,确切的说应该是 “算法的重用”(我理解的算法的重用是咱们在实现一个方法中,咱们只要去考虑如何去实现算法,而不须要考虑算法操做的数据类型的不一样,这样的算法实现更好的重用,泛型就是提供这样的一个机制)。工具
咱们在来看一个熟悉的算法——冒泡排序,冒泡排序中咱们能够对不一样类型的数据进行排序,其中,基本的算法逻辑是彻底相同的,仅仅是数据类型的不一样,咱们为了适应程序的灵活性,和重用性,咱们可使用泛型来定义一个排序的模子,对多种数据类型进行排序。post
class Program { static void Main(string[] args) { int[] array = {12,23,16,32,89,5}; SortHelper<int> sort = new SortHelper<int>(); sort.BubbleSort(array, (a,b) => a > b); Console.ReadKey(); } } public delegate bool Contrast<T>(T t1, T t2);//传入两个参数来做比较 public class SortHelper<T> { public void BubbleSort( T [] array, Contrast<T> contrast) { for (int i = 0; i < array.Length - 1; i++) { for (int j = 0; j < array.Length - 1-i; j++) { if (contrast(array[j] , array[j + 1]) ) { T temp = array[j]; array[j] = array[j+1]; array[j+1] = temp; } } } Console.WriteLine("排序后的数组"); for (int i = 0; i < array.Length - 1; i++) { Console.WriteLine("{0}", array[i]); } } }
运行结果:性能
然而泛型除了实现代码的重用的好处外,还有能够提供更好的性能和类型安全,下面经过下面一段代码来解释下为何有这两个好处的。ui
class Program { public static int constintListSize = 500000; static void Main(string[] args) { UseArrayList(); UseGenericList(); Console.ReadKey(); } private static void UseArrayList() { ArrayList list = new ArrayList(); long startTicks = DateTime.Now.Ticks; for (int i = 0; i < constintListSize; i++) { list.Add(i); } for (int i = 0; i < constintListSize; i++) { int value = (int)list[i]; } long endTicks = DateTime.Now.Ticks; Console.WriteLine("使用ArrayList,耗时:{0} ticks", endTicks - startTicks); } private static void UseGenericList() { List<int> list = new List<int>(); long startTicks = DateTime.Now.Ticks; for (int i = 0; i < constintListSize; i++) { list.Add(i); } for (int i = 0; i < constintListSize; i++) { int value = list[i]; } long endTicks = DateTime.Now.Ticks; Console.WriteLine("使用List<int>,耗时:{0} ticks", endTicks - startTicks); } }
使用ArrayList,耗时:468750 ticks
使用List<int>,耗时:156250 ticks
为何使用泛型的效率会这么高呢?接下来咱们一探究竟,用反翻译软件,咱们能够看出:
IL_001f: ldloc.1 IL_0020: ldloc.3 IL_0021: box [mscorlib]System.Int32 IL_0026: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) IL_002b: pop IL_002c: nop IL_002d: ldloc.3 IL_002e: ldc.i4.1 IL_002f: add
在上面的IL代码中,我用红色的标记的代码主要是在执行装箱操做(装箱过程确定是要消耗的事件的吧, 就像生活中寄包裹同样,包装起来确定是要花费必定的时间的, 装箱操做一样会,然而对于泛型类型就能够避免装箱操做,下面会贴出使用泛型类型的IL代码的截图)——这个操做也是影响非泛型的性能不如泛型类型的根本缘由。然而为何使用ArrayList类型在调用Add方法来向数组添加元素以前要装箱的呢?缘由其实主要出在Add方法上的, 你们能够用Reflector反射工具查看ArrayList的Add方法定义,下面是一张Add方法原型的截图:
从上面截图能够看出,Add(objec value)须要接收object类型的参数,然而咱们代码中须要传递的是int实参,此时就须要会发生装箱操做(值类型int转化为object引用类型,这个过程就是装箱操做),这样也就解释了为何调用Add方法会执行装箱操做的, 同时也就说明泛型的高性能的好处。
下面是使用泛型List<T>的IL代码截图(从图片中能够看出,使用泛型时,没有执行装箱的操做,这样就少了装箱的时间,这样固然就运行的快了,性能就行了。):
同时泛型可以提供的另外一个好处就是类型安全,这是什么意思呢?看下面一段代码:
ArrayList list = new ArrayList(); int i = 100; list.Add(i); string value = (string)list[0];
有读者一眼就能够看出这段代码有问题,由于类型不匹配,添加到ArrayList中的是一个int类型,而获取时却想将它转换为string类型。 惋惜的是,编译器没法知道,由于对它来讲,无论是int也好,string也好,它们都是Object类型。 在编写代码时,编译器提供给开发
者的最大帮助之一就是能够检查出错误,也就是常称的编译时错误(Compile timeerror)。 当使用ArrayList时,对于上面的问题,编译器无能为力,由于它认为其是合法的,编译能够顺利经过。 这种错误有时候隐藏在程序中很难发现,最糟糕的状况是产品已经交付用户,而当用户在使用时不巧执行到这段代码,便会抛出一个异常,这时的错误,称为运行时错误(Runtime error)。
经过使用泛型集合,这种状况将不复存在,当试图进行相似上面的转换时,根本没法经过编译,这样有助于尽早发现问题:
List<int> list = new List<int>(); int i = 100; list.Add(i); string value = (string)list[0]; //编译错误
泛型类型和其余int,string同样都是一种类型,泛型类型有两种表现形式的:泛型类型(包括类、接口、委托和结构,可是没有泛型枚举的)和泛型方法。那什么样的类、接口、委托和方法才称做泛型类型的呢 ?个人理解是类、接口、委托、结构或方法中有类型参数就是泛型类型,这样就有类型参数的概念的。 类型参数 ——是一个真实类型的一个占位符(我想到一个很形象的比喻的,好比你们在学校的时候,一到中午下课的时候食堂人特别多的,因此不少应该都有用书本占位置的习惯的, 书本就至关于一个占位符,真真坐在位置上的固然是本身的,讲到占位置,之前听过我同窗说,他们班有个很牛逼的MM,中午下完课的时候用手机占位子的,等它打完饭回来的时候手机已经不见, 当时听完我就和我同窗说,大家班这位女生真牛逼的,后面咱们就),泛型声明中,类型参数必须放在一对尖括号里面(即<>这个符号),而且用逗号分隔多个类型参数,如List<T>类中T就是类型参数,在使用泛型类型或方法的时候,咱们要用真实类型来代替,就像用书本占位子一个,书本只是暂时的在那个位置上,等打好饭了就要换成你坐在位置上了,一样在C#中泛型也是一样道理,类型参数只是暂时的在那个位置,真真使用中要用真实的类型去代替它的位置,此时咱们把真实类型又取名为类型实参,如上一专题的代码中List<int>,类型实参就是int(代替T的位置)。
若是没有为类型参数提供类型实参,此时咱们就声明了一个未绑定的泛型类型,若是指定了类型实参,此时的类型就叫作已构造类型(这里一样能够以书占位置去理解),然而已构造类型又能够是开放类型或封闭类型的,这里先给出这个两个概念的定义的:开放类型——具备类型参数的类型就是开放类型(全部的未绑定的泛型类型都属于开放类型的),封闭类型——为每一个类型参数都传递了实际的数据类型。对于开放类型,咱们建立开放类型的实例。
注意:在C#代码中,咱们惟一能够看到未绑定泛型类型的地方(除了做为声明以外)就是在typeof操做符里。
下面经过如下代码来更好的说明这点:
using System; using System.Collections.Generic; namespace CloseTypeAndOpenType { // 声明开放泛型类型 public sealed class DictionaryStringKey<T> : Dictionary<string, T> { } public class Program { static void Main(string[] args) { object o = null; // Dictionary<,>是一个开放类型,它有2个类型参数 Type t = typeof(Dictionary<,>); // 建立开放类型的实例(建立失败,出现异常) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<>也是一个开放类型,但它有1个类型参数 t = typeof(DictionaryStringKey<>); // 建立该类型的实例(一样会失败,出现异常) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<int>是一个封闭类型 t = typeof(DictionaryStringKey<int>); // 建立封闭类型的一个实例(成功) o = CreateInstance(t); Console.WriteLine("对象类型 = " + o.GetType()); Console.Read(); } // 建立类型 private static object CreateInstance(Type t) { object o = null; try { // 使用指定类型t的默认构造函数来建立该类型的实例 o = Activator.CreateInstance(t); Console.WriteLine("已建立{0}的实例", t.ToString()); } catch(Exception ex) { Console.WriteLine(ex.Message); } return o; } } }
运行结果为(从结果中也能够看出开放类型不能建立该类型的一个实例,异常信息中指出类型中包含泛型参数):
若是你们看了个人上一个专题的话,就应该会注意到我在实现泛型类的时候用到了where T : IComparable,在上一个专题并无和你们介绍这个是泛型的什么用法,这个用法就是这个部分要讲的类型约束,其实where T : IComparable这句代码也很好理解的,猜猜也明白的(若是是我不知道的话,应该是猜类型参数T要知足IComparable这个接口条件,由于Where就表明符合什么条件的意思,然而真真意思也确实如此的)下面就让咱们具体看看泛型中的类型参数有哪几种约束的。 首先,编译泛型代码时,C#编译器确定会对代码进行分析,若是咱们像下面定义一个泛型类型方法时,编译器就会报错:
// 比较两个数的大小,返回大的那个 private static T max<T>(T obj1, T obj2) { if (obj1.CompareTo(obj2) > 0) { return obj1; } return obj2; }
若是像上面同样定义泛型方法时,C#编译器会提示错误信息:“T”不包含“CompareTo”的定义,而且找不到可接受类型为“T”的第一个参数的扩展方法“CompareTo”。 这是由于此时类型参数T能够为任意类型,然而许多类型都没有提供CompareTo方法,因此C#编译器不能编译上面的代码,这时候咱们(编译器也是这么想的)确定会想——若是C#编译器知道类型参数T有CompareTo方法的话,这样上面的代码就能够被C#编译器验证的时候经过,就不会出现编译错误的(C#编译器感受很人性化的,都会按照人的思考方式去解决问题的,那是由于编译器也是人开发出来的,固然会人性化的,由于开发人员当时就是这么想的,因此就把逻辑写到编译器的实现中去了),这样就让咱们想对类型参数做出必定约束,缩小类型参数所表明的类型数量——这就是咱们类型约束的目的,从而也很天然的有了类型参数约束(这里经过对遇到的分析而后去想办法的解决的方式来引出类型约束的概念,主要是让你们能够明白C#中的语言特性提出来都是有缘由,并非说微软想提出来就提出来的,主要仍是由于用户会有这样的需求,这样的方式我以为可让你们更加的明白C#语言特性的发展历程,从而更加深刻理解C#,从我前面的专题也看的出来我这样介绍问题的方式的,不过这样也是我我的的理解,但愿这样引入问题的方式对你们会有帮助,让你们更好的理解C#语言特性,若是你们对于对于有任何意见和建议的话,均可以在留言中提出的,若是以为好的话,也麻烦表示承认下)。因此上面的代码能够指定一个类型约束,让C#编译器知道这个类型参数必定会有CompareTo方法的,这样编译器就不会报错了,咱们能够将上面代码改成(代码中T:IComparable<T>为类型参数T指定的类型实参都必须实现泛型IComparable接口):
// 比较两个数的大小,返回大的那个 private static T max<T>(T obj1, T obj2) where T:IComparable<T> { if (obj1.CompareTo(obj2) > 0) { return obj1; } return obj2; }
类型约束就是用where 关键字来限制能指定类型实参的类型数量,如上面的where T:IComparable<T>语句。C# 中有4种约束可使用,然而这4种约束的语法都差很少。(约束要放在泛型方法或泛型类型声明的末尾,而且要使用Where关键字)
(1) 引用类型约束
表示形式为 T:class, 确保传递的类型实参必须是引用类型(注意约束的类型参数和类型自己没有关系,意思就是说定义一个泛型结构体时,泛型类型同样能够约束为引用类型,此时结构体类型自己是值类型,而类型参数约束为引用类型),能够为任何的类、接口、委托或数组等;可是注意不能指定下面特殊的引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void.
以下面定义的泛型类:
using System.IO; public class samplereference<T> where T : Stream { public void Test(T stream) { stream.Close(); } }
上面代码中类型参数T设置了引用类型约束,Where T:stream的意思就是告诉编译器,传入的类型实参必须是System.IO.Stream或者从Stream中派生的一个类型,若是一个类型参数没有指定约束,则默认T为System.Object类型(至关于一个默认约束同样,就想每一个类若是没有指定构造函数就会有默认的无参数构造函数,若是指定了带参数的构造函数,编译器就不会生成一个默认的构造函数)。然而,若是咱们在代码中显示指定System.Object约束时,此时会编译器会报错:约束不能是特殊类“object”(这里你们能够本身试试看的)
(2)值类型约束
表示形式为T:struct,确保传递的类型实参时值类型,其中包括枚举,可是可空类型排除,(可空类型将会在后面专题有所介绍),以下面的示例:
// 值类型约束 public class samplevaluetype<T> where T : struct { public static T Test() { return new T(); } }
在上面代码中,new T()是能够经过编译的,由于T 是一个值类型,而全部值类型都有一个公共的无参构造函数,然而,若是T不约束,或约束为引用类型时,此时上面的代码就会报错,由于有的引用类型没有公共的无参构造函数的。
(3)构造函数类型约束
表示形式为T:new(),若是类型参数有多个约束时,此约束必须为最后指定。确保指定的类型实参有一个公共无参构造函数的非抽象类型,这适用于:全部值类型;全部非静态、非抽象、没有显示声明的构造函数的类(前面括号中已经说了,若是显示声明带参数的构造函数,则编译器就不会为类生成一个默认的无参构造函数,你们能够经过IL反汇编程序查看下的,这里就不贴图了);显示声明了一个公共无参构造函数的全部非抽象类。(注意: 若是同时指定构造器约束和struct约束,C#编译器会认为这是一个错误,由于这样的指定是多余的,全部值类型都隐式提供一个无参公共构造函数,就如定义接口指定访问类型为public同样,编译器也会报错,由于接口必定是public的,这样的作只多余的,因此会报错。)
(4)转换类型约束
表示形式为 T:基类名 (确保指定的类型实参必须是基类或派生自基类的子类)或T:接口名(确保指定的类型实参必须是接口或实现了该接口的类) 或T:U(为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数)。转换约束的例子以下:
声明 |
已构造类型的例子 |
Class Sample<T> where T: Stream |
Sample<Stream>有效的 Sample<string>无效的 |
Class Sample<T> where T: IDisposable |
Sample<Stream >有效的 Sample<StringBuilder>无效的 |
Class Sample<T,U> where T: U |
Sample<Stream,IDispsable>有效的 Sample<string,IDisposable>无效的 |
(5)组合约束(第五种约束就是前面的4种约束的组合)
将多个不一样种类的约束合并在一块儿的状况就是组合约束了。(注意,没有任何类型即时引用类型又是值类型的,因此引用约束和值约束不能同时使用)若是存在多个转换类型约束时,若是其中一个是类,则类必须放在接口的前面。不一样的类型参数能够有不一样的约束,可是他们分别要由一个单独的where关键字。下面看一些有效和无效的例子来让你们加深印象:
有效:
class Sample<T> where T:class, IDisposable, new();
class Sample<T,U> where T:class where U: struct
无效的:
class Sample<T> where T: class, struct (没有任何类型即时引用类型又是值类型的,因此为无效的)
class Sample<T> where T: Stream, class (引用类型约束应该为第一个约束,放在最前面,因此为无效的)
class Sample<T> where T: new(), Stream (构造函数约束必须放在最后面,因此为无效)
class Sample<T> where T: IDisposable, Stream(类必须放在接口前面,因此为无效的)
class Sample<T,U> where T: struct where U:class, T (类型形参“T”具备“struct”约束,所以“T”不能用做“U”的约束,因此为无效的)
class Sample<T,U> where T:Stream, U:IDisposable(不一样的类型参数能够有不一样的约束,可是他们分别要由一个单独的where关键字,因此为无效的)
参考资料:《NET之美》