这编译是没问题的,但这根本无法表明你的意思。你是真的想设计这样一个容器,用于存储总完全不同的元素吗?或者你是想在一个受到限制的语言上工作吗?这样的实践意味着当你移除集合里的元素时,你必须添加额外的代码来决定什么样的对象事先已经存在这样的集合中。不管什么情况,你须要从 System.Object强制转化这些元素到实际的你要的类型。
这还不只,当你把它们放到1.0版(译注:是1.0,不是1.1)的集合中时,值类型的开销更特殊。任何时候,当你放到个值类型数据到集合中时,你必须对它进行装箱。而当你在从集合中删除它时,你又会再开销一次。这些损失虽然小,但对于一个有几千元素的大集合来说,这些开销就很快的累积起来了。通过为每种不同的值类型生成特殊的代码,范型已经消除了这些损失。
如果你熟悉C++的模板,那么对于C#的范型就不存在什么问题了,因为这些从语法上讲是非常相似的。范型的内部的工作,不管它是怎产的,却是完全不同的。让我们看一些简单的例子来了解东西是如何工作的,以及它是如何实现的。考虑下面某个类的部份代码:
这些代码在集合中存储System.Object的引用,任何时候你都可以使用它,在你访问集合是,你必须添加强制转换。但使用C#范型,你可以这样定义同样的类:
你可以用对象来代替ItemType, 这个参数类型是用于定义类的。C#编译器在实例化列表时,用恰当的类型来替换它们。例如,看一下这样的代码:
List < int > intList = new List < int >();
MSIL可以精确的确保intList中存储的是且只是整数。比起目前你所实现的集合(译注:这里指C#1.1里的集合),创建的范型有几个好处,首先就是,如果你试图把其它任何不是整型的内容放到集合中时,C#的编译器会给出一个编译错误,而现今,你须要通过测试运行时代码来附加这些错误。
在C#1.0里,你要承担装箱和拆箱的一些损失,而不管你是从集合中移出或者是移入一个值类型数据,因为它们都是以System.Object的引用形式存在的。使用范型,JIT编译器会为集合创建特殊的实例,用于存储实际的值类型。这样,你就不用装箱或者拆箱了。还不只这些,C#的设计者还想避免代码的膨胀,这在C++模板里是相关的。为了节约空间,JIT编译器只为所有的引用类型生成一个版本。这样可以取得一个速度和空间上的平衡,对每个值类型(避免装箱)会有一个特殊的版本呢,而且引用类型共享单个运行时的版本用于存储System.Object (避免代码膨胀)。在这些集合中使用了错误的引用时,编译器还是会报告错误。
为了实现范型,CLR以及MSIL语言经历了一些修改。当你编译一个范型类时,MSIL为每一个参数化的类型预留了空间。考虑下面两个方法的申明MSIL:
这展示了C#编译器以及JIT编译是如何为一个范型而共同工作的。C#编译器生成的MSIL代码为每一个类型预留了一个空间,JIT编译器在则把这些预留的类型转换成特殊的类型,要么是为所有的引用类型用System.Object,或者对值类型言是特殊的值类型。每一个范型的变量实例化后会带有类型信息,所以C#编译器可以强制使用类型安全检测。
范型的限制定义可能会对你如何使用范型有很大的影响。记住,在CLR还没有加载和创建这些进行时实例时,用于范型运行时的特殊实例是还没有创建的。为了让MISL可以让所有的范型实例都成为可能,编译器须要知道在你的范型类中使用的参数化类型的功能。C#是强制解决这一问题的。在参数化的类型上强制申明期望的功能。考虑一个二叉树的范型的实现。二叉树以有序方式存储对象,也就是说,二叉树可以只存储实现了IComparable的类型。你可以使用约束来实现这一要求:
使用这一定义,使用BinaryTree的实例,如何使用了一个没有实现IComparable 接口的类型时是不能通过编译的。你可以指明多个约束。假设你想限制你的BinaryTree成为一个支持ISerializable的对象。你只用简单的添加更多的限制就行了。注意这些接口以及限制可以在范型上很好的使用:
限制同样可以提供一些更好的好处:编译器可以假设这些在你的范型类中的对象支持指定列表中的某些特殊的接口(或者是基类方法)。如何不使用任何限制时,编译器则只假设类型满员System.Object中定义的方法。你可能须要添加强制转换来使用其它的方法,不管什么时候你使用一个不在 System.Object对象里的方法时,你应该在限制集合是写下这些需求。
约束指出了另一个要尽量使用接口的原因(参见原则19):如果你用接口来定义你的方法,它会让定义约束变得很简单。
迭代也是一个新的语法,通常习惯上用于少代码。想像你创建一些特殊的新容器类。为了支持你的用户,你须要在集合上创建一些方法来支持逆转这些集合以及运行时对象。
目前,你可能通过创建一个实现IEnumerator了的类来完成这些。IEnumerator 包含两个方法,Reset和MoveNextand,以及一个属性:Current。另外,你须要添加IEnumerable来列出集合上所有实现了的接口,以及它的GetEnumerator方法为你的集合返回一个IEnumerator。在你写写完了以后,你已经写了一个类以及至少三个额外的函数,同时在你的主类里还有一些状态管理和其它方法。为了演示这些,目前你须要写这样一页的代码,来处理列表的枚举:
在这一方面上,C#2.0用yield关键字添加了新的语法,这让在写这些迭代时变得更清楚。对于前面的代码,在C#2.0里可是样的: