CLR类型设计之泛型(一)
在讨论泛型之前,我们先讨论一下在没有泛型的世界里,如果我们想要创建一个独立于被包含类型的类和方法,我们需要定义objece类型,但是使用object就要面对装箱和拆箱的操作,装箱和拆箱会很损耗性能,我们接下来会用一个示例来说明使用泛型和使用非泛型对值操作时的性能差距。但是如果使用泛型,也是同样的效果,不需要装箱和拆箱的同时泛型还保证了类型安全
言归正传,.Net自2.0以后就开始支持泛型,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型,此外,CLR还允许创建泛型接口和泛型委托。先来简单看一下泛型的语法。
一、为什么要有泛型?
我们在写一些方法时可能会方法名相同,参数类型不同的方法,这种叫做重载。如果只是因为参数类型不同里面做的业务逻辑都是相同的,那可能就是复制粘贴方法,改变参数类型,例如一些排序算法,int、float、double等类型的排序,参数数组存的数据类型不一样,还有像根据索引找到List集合中的对象。可能这个对象是Person、Dog等对象,这样方法改变的只是参数类型,那就是能不能写一个方法,传递不同的参数类型呢?于是有了泛型。
二、什么是泛型?
泛型通过参数化类型来实现在同一份代码上操作多种数据类型。例如使用泛型的类型参数T,定义一个类Stack<T>,可以用Stack<int>、Stack<string>或Stack<Person>实例化它,从而使类Stack可以处理int、string、Person类型数据。这样可以避免运行时类型转换或封箱操作的代价和风险,类似C++的模板。泛型提醒的是将具体的东西模糊化,这与后面的反射正好相反。
三、泛型的语法
需引用命名空间System.Collections.Generic,泛型List类后面添加了一个<T>,表明操作的是一个未指定的数据类型,在泛型声明过程中,所有的类型参数放在间括号中(<>),通过逗号分隔。
命名约定
- 泛型类型的名称用字母T作为前缀
- 如果没有特殊要求,泛型类型允许用任意类替代,且只使用了一个泛型类型就可以用字符T作为泛型类型名称,如下
public class List<T>{}
- 如果类型有特定的要求(例如必须实现一个接口或者派生自基类),或者使用了两个或两个以上泛型类型,就应给泛型类型使用描述性的名称,如下
Public class SortedList<Tkey,Tvalue>{}
泛型和非泛型性能对比
下面的示例说明,示例对比了使用泛型和不使用泛型的对比
1 static void Main(string[] args) 2 { 3 Stopwatch stopwatch = new Stopwatch(); 4 stopwatch.Start(); // 开始监视代码运行时间 5 List<long> listint = new List<long>(); 6 Add(listint); 7 stopwatch.Stop(); // 停止监视 8 TimeSpan timespan = stopwatch.Elapsed; 9 double milliseconds = timespan.TotalMilliseconds; 10 Console.WriteLine("泛型用时"+milliseconds); 11 12 13 Stopwatch stopwatch1 = new Stopwatch(); 14 stopwatch1.Start(); // 开始监视代码运行时间 15 ArrayList alistint = new ArrayList(); 16 Addf(alistint); 17 stopwatch1.Stop(); // 停止监视 18 TimeSpan timespan1 = stopwatch1.Elapsed; 19 double milliseconds1 = timespan1.TotalMilliseconds; 20 Console.WriteLine("非泛型用时"+milliseconds1); 21 22 } 23 24 public static void Add(List<long> a) 25 { 26 long sum = 0; 27 for (long i = 0; i < 1000000; i++) 28 { 29 a.Add(i); 30 } 31 foreach (var item in a) 32 {sum += item;} 33 Console.WriteLine("泛型集合结果"+sum); 34 } 35 36 37 public static void Addf(ArrayList a) { 38 long sum = 0; 39 for (long i = 0; i < 1000000; i++) 40 { 41 a.Add(i); 42 } 43 foreach (var item in a) 44 { sum+=Convert.ToInt32(item); } 45 Console.WriteLine("非泛型集合结果"+sum); 46 47 }
运行结果如下,在一百万次值类型循环下泛型要比非泛型省下50毫秒左右的时间,当然这点时间看起来很少,但是程序中往往不只是示例这样简单的逻辑,往往包含很对更加复杂的逻辑,时间和性能上的差距就会变得很大。
类型安全
从下面的例子可以看出,非泛型集合可以赋值任何类型,取出时只需要拆箱操作,泛型集合则需要赋值指定类型值,否则就会报错,从类型的安全角度来看,泛型的类型都是符合要求的类型才能放进来,所以更安全,而非泛型的集合则需要辨别其中那些是符合类型的,那些是不需要的
1 List<int> list = new List<int>();//创建泛型集合 2 ArrayList arry = new ArrayList();//非泛型集合 3 list.Add(1);//可以 4 list.Add("zj");//报错 5 6 arry.Add(1);//可以 7 arry.Add("张三");//可以
泛型类型
根据类型参数不同的指定类型实参的情况,泛型类型可以分为:
- 如果没有为类型参数提供类型实参,那么声明的就是一个未绑定泛型类型(unbound generic)
-
如果指定了类型实参,该类型就称为已构造类型(constructed type),然而已构造类型又可以是开放类型或封闭类型的
- 包含类型参数的类型就是开放类型(open type)(所有的未绑定的泛型类型都属于开放类型的),
- 每个类型参数都指定了类型实参就是封闭类型(closed type)
类型是对象的蓝图,我们可以通过类型来实例化对象;那么对于泛型来说,未绑定泛型类型是以构造泛型类型的蓝图,已构造泛型类型又是实际对象的蓝图。
下图就是一个简单的例子,Dictionary<TKey, TValue>就是一个泛型类型(未绑定泛型类型,开放类型);通过制定类型参数,可以得到不同的封闭类型;通过不同的封闭类型有可以构造不同的实例。
泛型类型和继承
泛型类型仍然是类型,所以能从其他任何类型派生,使用泛型类型并指定类型实参时,实际上在CLR中定义新的类型对象,新的类型对象从泛型类型派生自的那个类型派生,
换句话说,List<T>是从object派生,所以List<string>,List<int>也都派生自object,类似地,由于DictionaryStringKey<TValue>派生自Dictionary<String,TValue>所以DictionaryStringKey<Guid>派生自Dictionary<String,Guid>,类型实参的指定和继承层次结构没有任何关系--理解这一点,有助于判断哪些转型是能够进行的,哪些转型是不能进行的。
接下来看代码示例:
1 internal class Node{ 2 protected Node m_next; 3 public Node(Node next) { 4 m_next = next; 5 } 6 } 7 internal sealed class TypeNode<T> : Node { 8 public T m_data; 9 10 public TypeNode(T data) : this(data, null) { } 11 12 public TypeNode(T data, Node next) 13 : base(next) 14 { 15 m_data = data; 16 } 17 public override string ToString() 18 { 19 return m_data.ToString() + 20 ((m_next != null) ? m_next.ToString() : null); 21 } 22 } 23 24 程序入口: 25 Node head = new TypeNode<Char>('.'); 26 head = new TypeNode<DateTime>(DateTime.Now, head); 27 head = new TypeNode<String>("Today is ", head); 28 Console.WriteLine(head.ToString()); 29 程序输出: 30 Today is 2012-11-23 16:33:14.
定义一个非泛型的Node基类,在定义一个泛型TypedNode类(用Node类作为基类),这样依赖就可以创建一个链表,每个节点都可以是一种具体的数据类型(非Object类型),同时防止装箱,有点递归的意思。
代码爆炸
使用泛型类型参数的一个方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参进行替换,然后创建恰当的本地代码。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为"代码爆炸"。它可能造成引用程序集的显著增大,从而影响性能。
CLR内建了一些优化措施,能缓解代码爆炸。首先,假如为一个特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,CLR只会为这个方法/类型组合编译一次。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集也使用List<DateTime>,CLR只会为List<DateTime>编译一次方法。
CLR还提供了一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。之所以能这样,是因为所有引用类型的实参或变量时间只是执行堆上的对象的指针,而对象指针全部是以相同的方式操作的。
但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。因为值类型的大小不定。即使类型、大小相同,CLR仍然无法共享代码,可能需要用不同的本地CPU指令操作这些值。
结束语:
泛型的知识比预想的要多很多,一篇文章全部写完很长,会分为两个部分,将在二中继续研究泛型接口和泛型委托,协变和逆变泛型类型参数,泛型方法,和约束性
引用:http://www.cnblogs.com/wilber2013/p/4291435.html#_nav_0 田小计划 理解C#泛型
http://www.cnblogs.com/Ming8006/p/3789847.html#c.d 明-Ming 《CLR via C#》读书笔记 之 泛型
http://www.cnblogs.com/liuhailiang/archive/2012/11/26/2788642.html Lordbaby 泛型(三)泛型类型和继承