Effective C# 原则49:为C#2.0做好准备
C#2.0,在2005年已经可以使用了,它有一些主要的新功能。这样使得目前使用的一些最好的实际经验可能会有所改变,这也会随着下一代工具的发布而修改。尽管目前你还可以不使用这些功能,但你应该这些做些准备。
当Visual Studio .net2005发布后,会得到一个新的开发环境,升级的C#语言。附加到这门语言上的内容确实让你成为更有工作效率的开发者:你将可以写更好重用的代码,以及用几行就可以写出更高级的结构。总而言之,你可以更快的完成你的工作。
C#2.0有四个大的新功能:范型,迭代,匿名方法,以及部分类型。这些新功能的主要目的就是增强你,做为一个C#开发的开发效率。这一原则会讨论其中的三个新功能,以及为什么你要为些做准备。与其它新功能相比,范型在对你如何开发软件上有更大的影响。范型并不是C#的特殊产物。为了实现C#的范型,MS已经扩展了CLR以及MS的中间语言(MSIL)。C#,托管C++,以及VB.Net都将可以使用范型。J#也将可以使用这些功能。
范型提供了一种“参数的多太”,对于你要利用同一代码来创建一系列相似的类来说,这是一个很神奇的方法。当你为范型参数供一个特殊的类型,编译器就可以生成不同版本的类。你可以使用范型来创建算法,这些算法与参数化的结构相关的,它们在这些结构上实行算法。你可以在.net的Collection名字空间中找到很多候选的范型:HashTables, ArrayList,Queu,以及Stack都可以存储不同的对象而不用管它们是如何实现的。这些集合对于2.0来说都是很好的范型候选类型,这在System.Collections.Generic存在范型,而且这些对于目前的类来说都一个副本。C#1.0是存储一个对System.Obejct类型的引用,尽管当前的设计对于这一类型来说是可重用的,但它有很多不足的地方,而且它并不是一个类型安全的。考虑这些代码:
ArrayList myIntList = new ArrayList( );
myIntList.Add(32 );
myIntList.Add(98.6 );
myIntList.Add("Bill Wagner" );
这编译是没问题的,但这根本无法表明你的意思。你是真的想设计这样一个容器,用于存储总完全不同的元素吗?或者你是想在一个受到限制的语言上工作吗?这样的实践意味着当你移除集合里的元素时,你必须添加额外的代码来决定什么样的对象事先已经存在这样的集合中。不管什么情况,你须要从 System.Object强制转化这些元素到实际的你要的类型。
这还不只,当你把它们放到1.0版(译注:是1.0,不是1.1)的集合中时,值类型的开销更特殊。任何时候,当你放到个值类型数据到集合中时,你必须对它进行装箱。而当你在从集合中删除它时,你又会再开销一次。这些损失虽然小,但对于一个有几千元素的大集合来说,这些开销就很快的累积起来了。通过为每种不同的值类型生成特殊的代码,范型已经消除了这些损失。
如果你熟悉C++的模板,那么对于C#的范型就不存在什么问题了,因为这些从语法上讲是非常相似的。范型的内部的工作,不管它是怎产的,却是完全不同的。让我们看一些简单的例子来了解东西是如何工作的,以及它是如何实现的。考虑下面某个类的部份代码:
public class List
{
internal class Node
{
internal object val;
internal Node next;
}
private Node first;
public void AddHead( object t )
{
// ...
}
public object Head()
{
return first.val;
}
}
这些代码在集合中存储System.Object的引用,任何时候你都可以使用它,在你访问集合是,你必须添加强制转换。但使用C#范型,你可以这样定义同样的类:
public class List < ItemType >
{
private class Node < ItemType >
{
internal ItemType val;
internal Node < ItemType > next;
}
private Node < ItemType > first;
public void AddHead( ItemType t )
{
// ...
}
public ItemType Head( )
{
return first.val;
}
}
你可以用对象来代替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:
To implement generics, the CLR and the MSIL language undergo some
- When you compile a generic class, MSIL contains placeholders
- each parameterized type. Consider these two method declarations in
MSIL:
.method public AddHead (!0 t) {
}
.method public !0 Head () {
}
!0 就是一个为一个类型预留的,当一个实际的实例被申明和创建时,这个类型才创建。这就有一种替换的可能:
.method public AddHead (System.Int32 t) {
}
.method public System.Int32 Head () {
}
类似的,变化的实例包含特殊的类。前面的为整型的申明就变成了为样:
.locals (class List<int>)
newobj void List<int>::.ctor ()
这展示了C#编译器以及JIT编译是如何为一个范型而共同工作的。C#编译器生成的MSIL代码为每一个类型预留了一个空间,JIT编译器在则把这些预留的类型转换成特殊的类型,要么是为所有的引用类型用System.Object,或者对值类型言是特殊的值类型。每一个范型的变量实例化后会带有类型信息,所以C#编译器可以强制使用类型安全检测。
范型的限制定义可能会对你如何使用范型有很大的影响。记住,在CLR还没有加载和创建这些进行时实例时,用于范型运行时的特殊实例是还没有创建的。为了让MISL可以让所有的范型实例都成为可能,编译器须要知道在你的范型类中使用的参数化类型的功能。C#是强制解决这一问题的。在参数化的类型上强制申明期望的功能。考虑一个二叉树的范型的实现。二叉树以有序方式存储对象,也就是说,二叉树可以只存储实现了IComparable的类型。你可以使用约束来实现这一要求:
public class BinaryTree <ValType> where
ValType : IComparable <ValType>
{
}
使用这一定义,使用BinaryTree的实例,如何使用了一个没有实现IComparable 接口的类型时是不能通过编译的。你可以指明多个约束。假设你想限制你的BinaryTree成为一个支持ISerializable的对象。你只用简单的添加更多的限制就行了。注意这些接口以及限制可以在范型上很好的使用:
public class BinaryTree <ValType> where ValType : IComparable <ValType>, ValType : ISerializable
{
}
你可以为每个个实例化的类型指明一个基类以及任何数量的接口集合。另外,你可以指明一个类必须有一个无参数的构造函数。
限制同样可以提供一些更好的好处:编译器可以假设这些在你的范型类中的对象支持指定列表中的某些特殊的接口(或者是基类方法)。如何不使用任何限制时,编译器则只假设类型满员System.Object中定义的方法。你可能须要添加强制转换来使用其它的方法,不管什么时候你使用一个不在 System.Object对象里的方法时,你应该在限制集合是写下这些需求。
约束指出了另一个要尽量使用接口的原因(参见原则19):如果你用接口来定义你的方法,它会让定义约束变得很简单。
迭代也是一个新的语法,通常习惯上用于少代码。想像你创建一些特殊的新容器类。为了支持你的用户,你须要在集合上创建一些方法来支持逆转这些集合以及运行时对象。
目前,你可能通过创建一个实现IEnumerator了的类来完成这些。IEnumerator 包含两个方法,Reset和MoveNextand,以及一个属性:Current。另外,你须要添加IEnumerable来列出集合上所有实现了的接口,以及它的GetEnumerator方法为你的集合返回一个IEnumerator。在你写写完了以后,你已经写了一个类以及至少三个额外的函数,同时在你的主类里还有一些状态管理和其它方法。为了演示这些,目前你须要写这样一页的代码,来处理列表的枚举:
public class List : IEnumerable
{
internal class ListEnumerator : IEnumerator
{
List theList;
int pos = -1;
internal ListEnumerator( List l )
{
theList = l;
}
public object Current
{
get
{
return theList [ pos ];
}
}
public bool MoveNext( )
{
pos++;
return pos < theList.Length;
}
public void Reset( )
{
pos = -1;
}
}
public IEnumerator GetEnumerator()
{
return new ListEnumerator( this );
}
// Other methods removed.
}
在这一方面上,C#2.0用yield关键字添加了新的语法,这让在写这些迭代时变得更清楚。对于前面的代码,在C#2.0里可是样的:
public class List
{
public object iterate()
{
int i=0;
while ( i < theList.Length ( ) )
yield theList [ i++ ];
}
// Other methods removed.
}
yield语句让你只用6行代码足足替换了近30行代码。这就是说,BUG少了,开发时间也少了,以及少的代码维护也是件好事。
在内部,编译器生成的MSIL与目前这30行代码是一致的。编译器为你做了这些,所以你不用做 。编译器生成的类实现了IEnumerator 接口,而且添加了你要支持的接口到列表上。
最后一个新功能就是部分类型。部分类型让你要吧把一个C#类的实现分开到多个文件中。你可能很少这样做,如果有,你自己可以在日常的开发中,使用这一功能来创建多源文件。MS假设这一修改是让C#支持IDE以及代码生成器。目前,你可以在你的类中使用region来包含所以VS.net为你生成的代码。而将来(译注:指C#2.0),这些工具可以创建部份类而且取代这些代码到分开的文件中。
使用这一功能,你要为你的类的申明添加一个partial关键字:
public partial class Form1
{
// Wizard Code:
private void InitializeComponent()
{
// wizard code...
}
}
// In another file:
public partial class Form1
{
public void Method ()
{
// etc...
}
}
部分类型有一些限制。类只与源相关的,不管是一个文件还是多个源文件,它们所生成的MSIL代码没有什么两样。你还是要编译一个完整类的所有的文件到同样的程序集中,而且没有什么自动的方法来确保你已经添加了一个完整类的所有源文件到你的编译项目中。当你把一个类的定义从一文件分开到多个文件时,你可能会以引发很多问题,所以建议你只用IDE生成部分类型功能。这包含form,正如我前面介绍的那样。VS.Net同样为DataSet(参见原则41)也生成部分类型,还有web服务代理,所以你可以添加你自己的成员到这些类中。
我没有太多的谈到关于C#2.0的功能,因为添加的与目前的编码有一些冲突。你可以使用它,通过范型让你自己的类型变得简单,而定义接口可以描述行为:这些接口可以做为约束。新的迭代语法可以提供更高效的方法来实现枚举。你可以通过这一新语法,快速简单的取代嵌套枚举。然而,用户扩展类可能不会是简单的取代。现在开发你自己的代码,在显而易见的地方利用这些功能,而且在用C#2.0升级你已经存在的代码时,它会变得更容易,工作量也会变得最少。