首页 > 学院 > 开发设计 > 正文

[CLR via C#]12. 泛型

2019-11-17 03:20:55
字体:
来源:转载
供稿:网友

[CLR via C#]12. 泛型

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

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

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

  泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。

  泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。

  两者都是表示API的基本方法(不管是指一个泛型方法还是一个完整的泛型类型),以致平时期望出现一个普通类型的地方出现一个类型参数。比如,List<T>,在类名之后添加一个<T>,表明它操作的是一个未指定的数据类型。定义泛型类型和方法时,它为类型指定的任何变量(比如 T)都称为类型参数(type parameter)。T代表一个变量名,在源代码中能够使用一个数据类型的任何位置 ,都能使用T。

  类型参数是真实类型的占位符。在泛型声明中,类型参数要放在一堆尖括号内,并以逗号分隔。所以,在Dictionary<TKey, TValue>中,类型参数是TKey和TValue。使用泛型类型或方法时,要使用真实的类型代替。这些真实的类型称为类型实参(type argument)。

  泛型为开发人员提供了以下优势:

  1)源代码保护  使用一个泛型算法的开发人员不需要访问算法的源代码。然而,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。

  2)类型安全  将一个泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与制定数据类型兼容的对象才能随同算法使用。

  3)更清晰的代码  由于编译器强制类型安全性,所以减少了源代码中必须进行的转型次数。

  4)更佳的性能  在有泛型之前,要想定义一个常规化的算法,它的所有成员都要定义成操作Object数据类型。这其中就要有装箱和拆箱之间的性能损失。由于现在能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值的方式传递,CLR不再需要只需任何装箱操作。由于不再需要转型,所以CLR不必检查尝试一次转型操作是否类型安全,同样提高了代码的允许速度。

一、 Framework类库中的泛型

  泛型最明显的应用就是集合类。FCL已经定义了几个泛型集合类。其中大多数类能在Sysytem.Collections.Generic和System.Collections.ObjectModel命名空间中。要使用线程安全的泛型集合类,可以去System.Collections.Concurrent命名空间寻找。  Microsoft建议开发人员使用泛型集合类,并基于几个方面的原因,不鼓励使用非泛型集合类。首先,非泛型无法获得类型安全性、更清晰的代码和更佳的性能。其次,泛型具有更好的对象模型。  集合类实现了许多接口,放入集合中的对象也可能实现了接口,集合类可利用这些接口执行像排序这样的操作。FCL内建了许多泛型接口定义,所以在使用接口时,也能体会到泛型带来的好处。常用的接口包含在Sysytem.Collections.Generic命名空间中。    新的泛型接口并不是设计用来完全取代非泛型接口。  System.Array类(即所有数组的基类)提供了大量静态泛型方法,比如,AsReadonly、FindAll、Find、FindIndex等。

二、Wintellect的Power Collections库  Power Collections库由Wintellect制作,这个库有一系列集合类构成,任何人都可以免费下载和使用。  

集合类名称说明
BigList<T>有序T对象集合。操作100个以上的数据项是,效率非常高
Bag<T>无序T对象的集合,集合进行了哈希处理,并允许重复项
OrderedBag<T>有序T对象的集合,允许重复值
Set<T>无序T数据项集合,不允许重复项。添加重复项后,会只保留一个
OrderedSet<T>有序T数据项的集合,不允许重复项
Deque<T>双端队列(double-ending queue)。类似于一个列表,但在起始处添加/删除数据项时,比列表更高效
OrderedDictionary<TKey,TValue>字典,其中的键进行了排序,每个键都有一个对应的值
MultiDictionary<TKey,TValue>字典,其中每个键都可以有多个值,对键进行了哈希处理,允许重复,而且数据项是无序的
OrderedMultiDictionary<TKey,TValue>

字典,其中的键进行了排序,每个键都可以有多个值(同样进行了排序)。允许重复的键

三、泛型的基础结构

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

1)创建新的IL指令,使之能够识别类型实参 2)修改现有元数据表的格式,以便表示具有泛型参数的类型名称和方法 3)修改各种编程语言(C#等),以支持新的语法,允许开发人员定义个引入泛型类型和方法 4)修改编译器,使之能生成新的IL指令和修改元数据格式 5)修改JIT编译器,使之能够处理新的、支持类型实参的IL指令,以便生成正确的本地代码 6)创建新的反射成员,使开发人员能查询类型和成员,以判断它们是否具有泛型参数。另外,还必须定义新的反射成员,使开发人员能在运行时创建泛型类型和方法定义。 7)修改调试器以以显示和操作泛型类型、成员、字段以及局部变量。 8)修改VisualStudio 的"智能感知"(IntelliSense)特性。 1.开放类型封闭类型  前面我们讨论过CLR如何为应用程序的每个类型创建一个内部数据结构,这种数据结构称为类型对象。  具有泛型类型参数的类型仍然是类型,CLR同样会为它创建一个内部类型对象。无论是引用类型(类)、值类型(结构)、接口类型,还是委托类型,这一点都是成立的。  如果没有为任何类型参数提供类型实参,声明的就是一个未绑定泛型类型。  如果指定了类型实参,该类型就称为已构造类型。  我们知道,类型可以看做是对象的蓝图。同样的,未绑定泛型类型是已构造类型的蓝图。它是一种额外的抽象层。  已构造类型可以是开放类型封闭类型。  "开放类型"(open type)是指还包含一个类型参数,CLR禁止构造开放类型的任何实例。这一点类似于CLR禁止构造接口类型的实例。  代码引用一个泛型类型时,可指定一组泛型类型实参。假如为所有类型实参传递的都是实际数据类型,类型就称为"封闭类型"(closed type)。也就是说,具有泛型"类型实参"的类型称为"封闭类型"。CLR允许构造封闭类型的实例。  当代码引用一个泛型类型时,可能会留下一些泛型类型实参未指定。这会在CLR中创建一个新的开放类型的对象,而且不能创建该类型的实例。比如:
internal static class PRogram    {        private static void Main(string[] args)        {            Object o = null;             // Dictionary<,> 是一个开放类型,有两个类型参数            Type t = typeof(Dictionary<,>);             // 尝试创建该类型的一个实例 (失败)            o = CreateInstance(t);            Console.WriteLine();             // DictionaryStringKey<> 是一个开放类型,有一个类型参数            t = typeof(DictionaryStringKey<>);             // 尝试创建该类型的一个实例 (失败)            o = CreateInstance(t);            Console.WriteLine();             // DictionaryStringKey<Guid> 是一个封闭类型            t = typeof(DictionaryStringKey<Guid>);             // 尝试创建该类型的一个实例 (成功)            o = CreateInstance(t);             // Prove it actually worked            Console.WriteLine("Object type=" + o.GetType());             Console.ReadKey();        }         private static Object CreateInstance(Type t)        {            Object o = null;            try            {                o = Activator.CreateInstance(t);                Console.Write("已创建 {0} 的实例", t.ToString());            }            catch (ArgumentException e)            {                Console.WriteLine(e.Message);            }            return o;        }         // A partially specified open type        internal sealed class DictionaryStringKey<TValue> :            Dictionary<String, TValue>        {        }    }

  最后显示地结果为:

  可以看出,Activator的CreateInstance方法会在构造开发类型的实例时抛出一个ArgumentException异常。注意,在异常的字符串消息中,指明类型中仍然含有一些泛型参数。  从输出结果可以看出,类型名是以一个"`"字符和一个数字结尾的。这个数字代表类型的元数,也就是类型要求的类型参数的个数。例如,Dictionary类的元数为2,它要求为TKey和TValue这两个类型参数指定具体类型。  还要注意的是,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,这些字段不会在一个List<DataTime>和List<String>之间共享;每个封闭类型对象都有它自己的静态字段。另外,假如一个泛型类型定义了一个静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。在泛型类型上定义一个静态构造器的目的是保证传递的类型参数满足特定的条件。例如,如果希望一个泛型类型值用于处理枚举类型,可以如下定义:
internal sealed calss GenericTypeThatReqiresAnEnum<T> {    static GenericTypeThatReqiresAnEnum() {        if ( !typeof (T).IsEnum) {            throw new ArgumentException("T must be an enumerated type")        }    }}

  CLR提供了一个名为"约束"(constraint)的功能,可利用它更好地定义一个泛型类型来指出哪个类型实参是有效的。

 2.泛型类型和继承  泛型类型仍然是类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际上是在CLR中定义一个新的类型对象,新的类型对象是从派生该泛型类型的那个类型派生的。也就是说,由于List<T>是从Object派生的,那么List<String>和List<Guid>也是从Object派生的。 3. 泛型类型同一性  有的时候,泛型语法会将开发人员搞糊涂,所以有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有的类型实参。例如,为了简化一下代码:
List<DateTime> dt = new List<DateTime>();

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

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

然后就可以进一步简化创建:

DateTimeList  dt = new DateTimeList ();

  这样做表面上是方便了,但是决定不要单纯处于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。如下:

Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList));

  上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List<DateTime>传给它。然而,如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。

  C#提供一种方式,允许使用简化的语法来引用一个泛型封闭类型,同时不会影响类的相等性——使用using指令。比如:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;

  现在只想下面这行代码时,sameType会初始化为true:

Boolean sameType = (type(List<DateTime>) == (ypeof(DateTimeList));

  还有,可以使用C#的隐式类型局部变量功能,让编译器根据表达式的类型来推断一个方法的局部变量的类型。

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