首页 > 代码库 > 《CLR via C#》读书笔记 之 泛型

《CLR via C#》读书笔记 之 泛型

第十二章 泛型

2014-06-15

初始泛型
12.3 泛型基础结构
  12.3.1 开放类型与封闭类型
  12.3.2 泛型类型和继承
  12.3.3 泛型类型同一性
  12.3.4 代码爆炸
参考

初始泛型[1][2]


 返回 

泛型(generic)是CLR和编程语言提供一种特殊机制,它支持另一种形式的代码重用,即"算法重用"。

简单地说,开发人员先定义好一个算法,比如排序、搜索、交换等。但是定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以使用这个现成的算法了。

泛型有两种表现形式:泛型类型和泛型方法。

  • 泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。
  • 泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。

以最常用的FCL中的泛型List<T >为例:

 1 using System.Collections.Generic; 2  3 namespace Generics 4 { 5     class Program 6     { 7         static void Main(string[] args) 8         { 9             List<int> num = new List<int>();10             num.Add(1);11             num.Add(3);12             int num1 = num[0];13             int num2 = num[1];14         }15     }16 }

 

尖括号中的T是不确定的数据类型,叫做类型参数(type parameter),一般规定以字母T开头,可以是TKey, TValue都可以。而调用时指定的具体类型叫做类型实参(type argument)

类型参数是真实类型的占位符。

 

 

  • 类型名List是以“`”加数字结尾的。数字表示类型的元数,也就是需要指定具体类型的参数个数。
  • 泛型是类型安全的。如果用“num.Add("a");”会发生编译错误;
  • 泛型可以提高算法的可重用性,而且从例子中看出int类型并没有进行装箱拆箱操作,相比将所有类型转换为Object的方式而言,提高了程序的性能。
  • 为泛型变量设置默认值时常使用default关键字进行,T temp=default(T)。如果T为引用类型,则temp为null;如果T为值类型,则temp设为0值. 

12.3 泛型基础结构[2]


 返回  

为了是泛型能够工作,Microsoft必须完成以下工作:

  • 创建新的IL指令,使之能够识别类型实参
  • 修改现有元数据表的格式,以便表示具有泛型参数的类型名称和方法
  • 修改各种编程语言(C#等),以支持新的语法,允许开发人员定义个引入泛型类型和方法
  • 修改编译器,使之能生成新的IL指令和修改元数据格式
  • 修改JIT编译器,使之能够处理新的、支持类型实参的IL指令,以便生成正确的本地代码
  • 创建新的反射成员,使开发人员能查询类型和成员,以判断它们是否具有泛型参数。另外,还必须定义新的反射成员,使开发人员能在运行时创建泛型类型和方法定义。
  • 修改调试器以以显示和操作泛型类型、成员、字段以及局部变量。
  • 修改VisualStudio 的"智能感知"(IntelliSense)特性。

 

12.3.1 开放类型与封闭类型

开放类型:具有泛型参数的类型是开放类型,如List<T>,CLR不允许构造开放类型的实例;
封闭类型:在实际调用代码时,如果所有类型实参都已经指定了实际数据类型,如List<string>,则该类型为封闭类型。CLR允许构造封闭类型的实例。

12.3.2 泛型类型和继承

泛型类型仍然是类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际上是在CLR中定义一个新的类型对象,新的类型对象是从派生该泛型类型的那个类型派生的。也就是说,由于List<T>是从Object派生的,那么List<String>和List<Guid>也是从Object派生的。

12.3.3 泛型类型同一性

有的时候,泛型语法会将开发人员搞糊涂,所以有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有的类型实参。例如,为了简化一下代码:

List<DateTime> dt = new List<DateTime>();

一些开发人员可能首先定义下面这样的一个类:

1 internal sealed class DateTimeList : List<DataTime> 2 {3         //这里无需放任何代码!4 }

然后就比较一下DateTimeList和List<DateTime>的同一性:

1         static void Main(string[] args)2         {3             DateTimeList dt = new DateTimeList();4             Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList))); 5         }

上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。

因此,决不要单纯处于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List<DateTime>传给它。然而,如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。 

12.3.4 代码爆炸

 使用泛型类型参数的一个方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参进行替换,然后创建恰当的本地代码。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为"代码爆炸"。它可能造成引用程序集的显著增大,从而影响性能。
 
CLR内建了一些优化措施,能缓解代码爆炸。首先,假如为一个特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,CLR只会为这个方法/类型组合编译一次。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集也使用List<DateTime>,CLR只会为List<DateTime>编译一次方法。
 
CLR还提供了一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。之所以能这样,是因为所有引用类型的实参或变量时间只是执行堆上的对象的指针,而对象指针全部是以相同的方式操作的。
 
但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。因为值类型的大小不定。即使类型、大小相同,CLR仍然无法共享代码,可能需要用不同的本地CPU指令操作这些值。


 

参考

[1] 跟小静读CLR via C#(16)--泛型 http://www.cnblogs.com/janes/archive/2011/12/21/2295959.html
[2] 《CLR via C#》读书笔记(7) -- 泛型(上) http://www.cnblogs.com/Code-life/archive/2012/12/20/2823520.html