进阶系列(1)——泛型
一、泛型的是什么
泛型的英文解释为generic,当然我们查询这个单词时,更多的解释是通用的意思,然而有些人会认为明明是通用类型,怎么成泛型了的,其实这两者并不冲突的,泛型本来代表的就是通用类型,只是微软可能有一个比较官方的词来形容自己引入的特性而已,既然泛型是通用的, 那么泛型类型就是通用类型的,即泛型就是一中模子。 在生活中,我们经常会看到模子,像我们平常生活中用的桶子就是一个模子,我们可以用桶子装水,也可以用来装油,牛奶等等,然而把这些都装进桶子里面之后,它们都会具有桶的形状(水,牛奶和油本来是没有形的),即具有模子的特征。同样,泛型也是像桶子一样的模子,我们可以用int类型,string类型,类去实例化泛型,实例化之后int,string类型都会具有泛型类型的特征(就是说可以使用泛型类型中定义的方法,如List<T>泛型,如果用int去初始化它后,List<int>的实例就可以用List<T>泛型中定义的所有方法,用string去初始化它也一样,和我们生活中的用桶装水,牛奶,油等非常类似
二、C# 2.0为什么要引入泛型
大家通过第一部分知道了什么是泛型,然而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中为什么要引入泛型的,然而泛型可以给我们带什么好处的呢?从上面的例子可以看出,泛型可以帮助我们实现代码的重用,大家很清楚——面向对象中的继承也可以实现代码的重用,然而泛型提供的代码的重用,确切的说应该是 “算法的重用”(我理解的算法的重用是我们在实现一个方法中,我们只要去考虑如何去实现算法,而不需要考虑算法操作的数据类型的不同,这样的算法实现更好的重用,泛型就是提供这样的一个机制)。
我们在来看一个熟悉的算法——冒泡排序,冒泡排序中我们可以对不同类型的数据进行排序,其中,基本的算法逻辑是完全相同的,仅仅是数据类型的不同,我们为了适应程序的灵活性,和重用性,我们可以使用泛型来定义一个排序的模子,对多种数据类型进行排序。
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]); } } }
运行结果:
然而泛型除了实现代码的重用的好处外,还有可以提供更好的性能和类型安全,下面通过下面一段代码来解释下为什么有这两个好处的。
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之美》