技术掘金,专注于Web互联网开发,分享技术,致力于打造好的技术博客
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 somechanges. When you compile a generic class, MSIL contains placeholdersfor each parameterized type. Consider these two method declarations inMSIL:.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升级你已经存在的代码时，它会变得更容易，工作量也会变得最少。
对于C#以及.Net来说这是激动人心的时候。这些工具目前还是比较新的，整个社区都在学习如何使用这些工具。一些资源可以帮助你提高你的知识，以及为.Net和C#创建一个更大的知识社区。这些工具是我每天都向C#开发人员推荐的。关于C#实践的全部内容还在写作当中，跟进它们而且不断了解相关的内容。第一个应该在每一个C#开发人员的工具箱的工具是NUnit, 它可以在www.nunit.org网站上找到。NUnit是一个自动进行单元测试的工具，功能和JUnit很像。和其它大多数开发人员一样，我讨厌写测试代码并且自己测试。NUnit让这些进程都变得很高效，在你有规律的使用这些工具后，可以保证你会习惯测试你所有的C#类。不管什么时候当我创建了一个类库工程时，我都会添加一个NUnit测试工程，而且把自动生成的测试做为一部分添加进来。我添加一个以创建和运行测试的配置，这样可以在每次编译时进行测试。然后，我可以转换活动的配置来控制是否要让单元测试做为正规程序的一部份存在。默认情况下，我运行它们。当我须要进行UI测试时，我会转换到另一个配置上。在附带的使用NUnit时，你可以通过检测NUnit的源代码学到一些有意思的技术。NUnit使用一些高级的反射习惯来加载和测试你的程序集。它使用特性来查找测试包，测试用例，以及每个测试用例的期望结果(参见原则42)。这是一个非常不错的例子，可以告诉你如何使用这些技术来创建可以自己动态配置的工具，而且它可以广泛的应用。接来下是FXCop，这是一个免费的工具，可以从GotDotNet(www.gotdotnet.com)上得到。FXCop 分析你的程序集里的IL，看它是否与实践的原则相违背，以及报告这些违例的地方。每一个原则都有一个可靠的公制规范，以及使用这一原则的原因。如本书里所有推荐的原则一样，一些文档中有一个关于某一原则的简短理由。你可以断定这些实际的问题是否遵守这些建议。同样你也可以配置是否把每一个原则都应用到项目中。我并不赞成FXCop 中的一些原则，而且我在本书前面已经说明了原因。然而，像NUnit一样，FXCop 可以成为你创建的正规程序的一部分。每次编译后，可以有一个编译后步骤，可以用FXCop 来分析你选择的原则。图6.1展示了一个从FXCop里输出的例子。尽管一些推荐并不是我喜欢的(例如有一个是让每一个程序集应该是让COM见的)，但它确实是一个有用的工具，因为它让你思考很多你已经默认的决定。图6.1,FXCop分析的一个项目：Figure 6.1. FXCop analyzing a project.ILDasm是一个IL反汇编器，在本书不同的地方，我已经演示了一些IL代码，它们就是编译器为不同的C#结构生成的。尽管我不相信很多人会在有高级语言存在的情况下，还选择写IL代码，但你应该熟悉它。知道从不同的C#结构上生成的IL代码，可以帮助你成为一个更好的开发者。你可以为你自己的程序集检测IL代码，或者是.Net框架里的程序集。这就是使用ILDAsm，而且它是和.Net框架的SDK一起发布的。IL对于所有开发者来说都是可用的。ILDAsm可以让你看到你的程序集的中间语言。不管怎样，这是一个好的方法来学习.Net框架程序集，这也是得到的原始资料。这些只是你正式工具箱中的一部份，但拥有这些工具只是提高你技能的一个方面。大量在线的资源以及交流社区可以让你参与和学习，以及增加你自己的C#和.net框架知识。首先也是最重要的就是GotDotNet 网站(www.gotdotnet.com)，这是.Net组的官方网站。C#小组在MSDN上有一个站点，目前是在msdn.microsoft.com/vcsharp/ (它偶然在MSDN网站有变动而被重新组织)。如果你的工作主要是基于网络的，试着访问www.asp.net，这是为ASP.Net组提供的。如果你的工作主要是基于Windows Form的，试着看看www.windowsforms.net，这是Windows Form组的官方网站。这些网站包含很多常规编程的引用和实现，这些可能是你的应用程序中想要的。它们都是还源文件的组件，所以你可以检测和修改这些，让它们成为你想要的。最后也是最重要的位置应该要了解就是在MS模式和实践的网页。这个网页目前在 www.microsoft.com/resources/practices/，从这个地方，你可以查到一些常用的设计模式以及一些最好的模式的初始代码。而且这个地方经常更新一些新的例子的代码以及库，这可能帮助你解决常规的编程问题。在写这些时，你已经可以使用10个不同的应用程序块来实现一些常规的程序要求，我确信当你阅读到这些时，这些地方已经有更多的内容了。我还要推荐一些C#组的FAQ的订阅：http://blogs.msdn.com/csharpfaq，附带的在这个上，有几个C#组的成员用博客讨论一些C#问题。你可以在这里找到最新的列表： http://msdn.microsoft.com/vcsharp/team/blogs/如果你学习更多的而且对语言和环境想得到更深入的了解，你可以检测共享的CLI(code-named rotor)。这包含.net框架以及C#编译器的一些核心内容。你可阅读这些资料来对C#语言的每一个功能和.Net框架得到更深入的理解。并不是所有的.net商业框架都有可用的共享资料：例如，特殊的Windows代码并没有发布共享代码。然而，这些已经发布了的子集，同样够你学习更多的关于CLR和C#语言内部的东西。C#编译器已经和共享的CLI资料一起发布，这是用C++写的，它做为底层的CLR代码存在。你须要对C++有很深的背景知识，以及对编译器设计有清楚的认识才能很好的理解它。现代的语言编译器是复杂的软件块，但CLR资料是一个有用的工具，来理解.Net框架的核心功能是如何实现的。这里只是给出了一个简单的列表， 我只是在众多资料中介绍了一个表面。很多资料你都可以多MS上得到，或者其它在线网站，或者是书。你越是多的使用这些工具，你就可以得到越多的知识。整个.Net以及C#社区是在前进的，因为它发展的很快，这些列出的资源也可能不断的在改变。你可以自己学习和自己写稿。
.Net运行时已经设计好了，一些怀有恶意的代码不能渗透到远程计算机上并执行。目前一些分部式系统依懒于从远程机器上下载和执行代码。如果你可以通过Internet或者以太网来发布你的软件，或者直接从web上运行，但你须要明白CRL在你的程序集中的一些限制。如果CLR不是完全相信一个程序集，它会限制一些的行为。这些调用代码要有访问安全认证(CAS)。从另一方面来说，CLR强制要求基于角色的安全认证，这样这些代码才能或者不能在基于一个特殊的角色帐号下运行。安全违例是运行时条件，编译器不能强制它们。幸运的是，它们绝不会在你的开发机器上出现，而且你所编译的代码从你自己的硬件上加载，这就是说，它有更高的信任级别。讨论所有潜在的.Net安全模型可以足足写上几本书，但你可以了解合理行为的一小部份，这样可以让你的程序集与.Net的安全模式更容易的交互。这些推荐只有在你创建一个组件程序库时，或者是开发一些通过网络发布的组件和程序集是才可以参考应用。通过这个讨论，你应该记住.Net是一个托管的环境。这个环境保证有一个明确的安全环境。在安装时可以用.Net的配置策略来管理安全策略。大多数.Net框架库是在安装时对配置策略是安全信任的。它会查明安全问题，这就是说CLR可以检测IL而且确保它不会有什么潜在的危险行为，例如直接访问原始内存。它不会在访问本地资源时要求特殊的安全权限进行断言。你应该试着遵守同样的检测，如果你的代码不须要任何的安全权限，就应该避免使用CAS的API来对断定访问权限，否则你所做的只是降低程序性能。你要使用CAS的API来访问一些受保护的资源，而这些资源是要求增加的特权的。很多通用的受保护资源是非托管的内存和文件系统。其它一些受保护的资源还包括数据库，网络端口，windows注册表，以及打印子系统。在每种情况下，如果调用代码没有足够的许可，试着访问这些资源都会引发一个异常。而且，访问这些资源可能引发运行时建立一个安全栈上的询访，以确保当前栈上的所有的程序集有恰当的许可。让我们看一下内存以及文件系统，讨论安全系统和机密问题中最实际的一些问题。不管什么时候，你都可以通过创建恰当的安全程序集来避免非托管内存访问。一个安全的程序集，也就是一个不用使用任何指针来访问其它非托管，或者托管的堆内存。不管你是否知道，你所创建的所有C#代码几乎都是安全的。除非你在C#编译器上打开了不安全的编译开关/unsafe，否则你所创建的都是安全代码(译注：就算打开了开关也不是说就编译成不安全代码了，还要看你的代码是怎样写的。)。/unsafe充许用户使用CLR不进行的验证的指针。要使用不安全代码的原因很少，特别是一常规的任务中。指向原始内存的指针比要检测的安全的引用快一些。在一些经典的数组中，它们可能要快上10倍以上。但当你使用不安全结构时，要明白任何的不安全代码都会影响整个程序集。当你创建不安全块时，应该考虑把这些算法独立到一个程序信中(参见原则32)。这样可以在整个程序上限制不安全代码的影响。如果它是独立的，只有实际调用它的访问者才会受到影响。其它剩下的，你还是可以在更严格的环境中使用安全机制。你可能还须要不安全代码来处理一些须要直接指针的P/Invoke或者COM接口。同样的推荐：独立它。不安全代码只会影响它自己的小程序集，不再有其它的。对于访问的建议很简单：只要可能，都应该避免访问非托管内存。接下来的安全核心就是文件系统。程序要存储数据。从Internet上下载回来的代码，文件系统中的大多数地方都不能访问，否则会有很大的安全漏洞。是的，完全不许访问文件系统就很难创建能使用的程序。通过使用独立存储可以解决这一问题。独立存储可以穿越基于程序集而独立的虚拟的目录，以及应用程序域，以及当前的用户。选择性的，你可以使用更一般的独立存储虚拟目录，该目录是基于程序集或者当前用户的。实际上，受信任的程序集可以访问他们自己特殊的独立存储区域，但不能是文件系统的其它地方。独立的存储目录是隐藏在其它程序集以及其它用户中的。你可以使用System.IO.IsolatedStorage名字空间中的类来访问独立的存储。IsolatedStorageFile类包含的方法可以很简单的访问System.IO.File类。实际上，它是从 System.IO. FileStream 类派生下来的。写内容到独立存储的代码几乎与写内容到任何文件里是一样的：IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForDomain( ); IsolatedStorageFileStream myStream = new IsolatedStorageFileStream( "SavedStuff.txt", FileMode.Create, iso ); StreamWriter wr = new StreamWriter( myStream ); // several wr.Write statements elided wr.Close(); 读操作也是完全和其它使用文件I/O相似的：IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForDomain( ); string[] files = isoStore.GetFileNames( "SavedStuff.txt" ); if ( files.Length > 0 ) { StreamReader reader = new StreamReader( new IsolatedStorageFileStream( "SavedStuff.txt", FileMode.Open,isoStore ) ); // Several reader.ReadLines( ) calls elided. reader.Close(); } 你可以独立存储来持久大小合适的数据元素，这些元素可以被代码部分信任，用于从一个安全分离的本地磁盘上的某个地方存储和载入信息。.Net环境为每个程序定义和限制了独立存储的大小。这可以预防一些恶意的代码占用磁盘空间，让系统就得不可用。独立存储对于其它程序和其它用户来说是不可见的。也就是说，它不应用于要管理员手动操作才能布署或者配置设置的情况。即使这它是隐藏的，然而，独立存储对于从受信任的用户那来里来的非托管代码来说也是不受保护的。不要用独立存储来存储一些高度机密的内容，除非你的程序给它加过密。在文件系统中创建一个可能要许可安全策略的程序集时，要独立存储流的内容。当你的程序集可能在web上运行，或者可能被运行在web上的代码访问时，应该考虑使用独立存储。你可能须要正确的使用一个受保护的资源。一般情况下，访问这些资源要指出你的程序要被完全信任。唯一可选的就是完全避免使用这些受保护的资源。例如，考虑windows的注册表，如果你和程序须要访问注册表，你必须安装你的程序到最终用户的机器上，这样才能有必须的权限来访问注册表。你想简单点，从web上运行的程序是不能建立注册表的修改的。安全策略就应该是这样的。.Net的安全模型意味着你的程序的行为是要权得进行核对的。注意你的程序所要求的权利，而且试着最小化它们。不必要求你不使用的权利。你的程序集越是少的要求受保护资源，那么它们就越是可以保证安全策略异常不抛出。避免使用机密资源，如果可能，要考虑其它可选方案。当你在某个算法上确实须要更高安全的许可时，应该独立这些代码到它们自己的程序集中。
Effecitve C# 原则46：最小化与其它非托管代码的交互
在开发设计.Net时，MS所做的最聪明的修改之一就是他们意识到，如果没有办法整合已经存在的代码到新的.Net环境中，那没没有人会接受这个新的平台。MS知道，如果没有办法来利用已经存在的代码，这将阻止大家接受它。与其它非托管代码的交互是可以工作了，但这是可交互唯一可以拿来说一下的有利的地方。对于所有的交互策略，当操作流程在本地代码和托管代码之间的边界上进行传送时，都要求强制提供一些 编组的信号。同时，交互策略强迫开发人员必须手动的申明所有调用参数(译注：这里是说你根本不知道参数的数据类型，很多时间你都只能以int32的方式传递所有参数，例如文件句柄，字符串指针等几乎是所有的参数，都只有一个int32也就是IntPtr类型进行处理，当然这里认为是是在32位机器上。)。最后，CLR还不能完成对托管代码和本地代码的边界上进行数据传递时的优化。忽略采用本地代码或者COM对象时得到的好处吧，没有什么比这更好的了(译注：我本人强烈反对这一原则。C++，COM在目前来说，绝对有它生存的优势，我觉得应该充分利用这些优势，而不应该忽略它们)。但事实是交互并不是总能工作的，很多时候我们还是在要已经存在的应用程序中添加新的功能，提高而且更新已经存在的工具，或者在其它的地方要完成一个新的托管应用程序与旧的应用程序交互。使用一些交互在实际应用中只会是减缓对旧系统的更替。所以，有明白不同的交互策略之间有什么开销是很重要的。这些开销要同时花在开发计划以及运行时性能中。有些，最后的选择是重写旧代码。还有一些时候，你须要选择正确的交互策略。在我讨论这个对你有用的交互策略之前，我须要花一段来讨论放弃(just throw it out)策略。第五章，与.Net框架一起工作，向你展示了一些.Net里已经为你创建好了的类和技术，你可以直接使用或者派生。为你你想的很多，你可以确定一些类和你一些代码算法，并且全部用C#重写。剩下存在的代码可以被.Net框架里已经存在的可能功能性的派生来取代。这并不会总是在任何地方，任何时候都可以工作的，但这确实是一个经过认真考虑过的迁移策略。整个第5章都推荐使用"throw it out“策略。这一原则就专注于交互，而它确实是件痛苦的事情。接下来，让我们假设你已经决定重写全部代码并不实际。一些不同的策略要求你从.Net中访问本地代码。你须要明白在本地代码和托管代码的边界上传递数据时的开销的低效。在使用交互时有三个开销。首先就是数据集群处理，这在托管堆和本地代码堆之间进行数据传递时发生。其次就是在托管代码和非托管代码进行交互时的大量数据吞吐时的开销。你以及你的用户要承担这些开销。第三个开销就只是你自己的了：你要在这个混合的开发环境中添加很多工作来实现交互。这也是最糟糕的一个，所以你的设计应该决定最小化这样的开销。让我们开始讨论交互时在性能上的开销，以及如何最小化这些开销。数据集群是最大的一个因数，就像是网络服务或者远程操作一样，你须要尽可能使用笨重的(chunky)API而不是小巧的(chatty )API(译注：数据集群是指你没有办法即时的与本地代码进行交互，而有一个延时，这个延时就使用数据堆集起来一起处理，这样就使得你应该尽可能少的与本地代码进行交互，而要选择一些一次可以处理较多数据的API)。你可以用不同的方法来完成与非托管代码的交互。你可以重新修改已经存在的非托管代码来创建一个笨重的API，更适合交互的API。常规的COM应用中是申明很多属性，这样客户可以设置并修改COM对象内部的状态或者行为。每次的设置属性都会集群数据，而且不得不穿越边界。(而且每在穿越交互边界时也会有thunks。)这非常的低效，不幸的是，COM对象或者非托管库可能不受你控制。当这种情况发生时，你须要完成更麻烦的工作。这时，你可以创建一个轻量级的C++库，通过使用你所须要的chunkier API来暴露类型的功能。这就要增加你的开发时间了(也就是第三个开销)。当你封装一个COM对象时，确保你修改的数据类型已经在本地代码一托管代码之间提供了最好的数据集群策略。有些类型可以很好的比其它类型进行集群，试着限制用于在本地代码和托管代码之间进行传递的数据类型，尽量使用blittable数据。blittable是指托管代码和本地代码都一样使用的类型。数据内容可以直接拷贝而不用管对象的内部的结构。某些情况下，非托管代码可能使用托管代码的代码。下面列出了blittable 类型：System.Byte System.SByte System.Int16 System.UInt16 System.Int32 System.UInt32 System.Int64 System.UInt64 System.UIntPtr 另外，任何的blittable类型的一维数组也是blittable类型。最后，任何格式化的包含blittable类型的也是blittable类型。一个格式化的类型可以是一个用StructLayoutAttribute明确定义了数据层次的结构，[ StructLayout( LayoutKind.Sequential ) ] public struct Point3D { public int X; public int Y; public int Z; } 当你在托管代码和非托管代码之间，只使用blittable 类型时，你就最小化了多少必须拷贝的信息呀!你同样也优化了任何必须发生的拷贝操作。如果在数据拷贝时，你不能限制数据类型让它成为blittable 类型，你可以使用InAttribute 和OutAttribute 来进行控制。也COM类似，这些特性控制数据拷贝的方法。In/Out 参数双向拷贝，In参数以及Out参数是一次拷贝。确保你应用严格限制的In/Out组合，来避免更多不必须拷贝。最后，你可以通过申明如何集群数据来提高性能。对于字符串来说这是最常见的。集群字符串默认是使用BSTRs。这是一个安全的策略，但这也是最低效的。你可以通过修改默认的集群格式减少额外的拷贝操作，可以使用MarshalAs 特性来修改集群方式。下面申明了字符串的集群使用LPWStr或者wchar*:public void SetMsg( [ MarshalAs( UnmanagedType.LPWStr ) ] string msg ); 这有一个关于处理托管和非托管层上数据的轶事：数据被拷贝然后在托管和非托管类型之间进行传输。你有三个方法业最小化拷贝操作。首先就是通过限制参数和返回值，让它们是blittable类型。这应该是首选的。当你不能这样做时，应用In和Out特性来最小化必须完成的拷贝和传输操作。最后一个优化就是，很多类型可以不只一种集群方式，你应该选择最优化的一种。现在让我们转到如何在托管的非托管组件中转移程序控制。你有三种选择：COM交互，平台调用(P/Invoke)，以及托管C++。每种方法有它自己的优势和劣势。COM交互是最简单的方法来使用已经存在的COM组件。但COM交互也是在.Net中和本地代码交互时最低效的一种方式。除非你的COM组件已经有很重要的利益，否则不要这样做。不要看也不要想这种方式。如果你没有COM组件而要使用这种方法就意味着你要把COM和交互原则学的一样好。没时间让你开始理解IUnknown(译注：COM原理中最基本的接口，任何COM都实现了这样的接口)。那些这样做的人都试着从我们的内存中尽快的清理它们。使用COM交互同样意味着在运行时你要为COM子系统承担开销。你同样还要考虑，在不同的CLR对象的生命期管理和COM版本的对象生命期管理之间又意味看什么。你可以服从CLR的原则，这就是说做一个你所导入的COM对象有一个析构函数，也就是在COM接口上调用的Release()。或者你可以自己清楚的使用ReleaseCOMObject()来释放COM对象。第一种方法会给应用程序引入运行时的低效(参见原则15)。而第二个在你的程序里又是头疼的事。使用ReleaseCOMObject ()就意味看你要深入到管理细节上，而这些CLR的COM交互已经帮我们完成了。你已经了解了，而且你认你明白最好的。CLR请求所有不同，而且它要释放COM对象，除非你正确的告诉它，你已经完成了。这是一个极大的鬼计，因为COM专家程序员都是在每个接口上调用Release()，而你的托管代码是以对象处理的。简单的说，你必须知道什么接口已经在对象上添加了AddRef，而且只释放这些(译注：COM的引用非常严格，每个引用都会添加一个AddRef，释放时必须明确的给出Release()，而且必须成对使用，而在.Net对COM进行封装后，很多时候就是这个引用不匹配而出现资源泄漏)。就让CLR来管理COM的生命期，你来承担性能损失就行了。你是一个烦忙的开发人员，想在.Net中混合COM资源到托管环境里，你要学习的太多了(也就是第三个开销)。第二个选择是使用P/Invoke。这是一个更高效的方法来调用Win32的API，因为你避免了在上层与COM打交道。而坏消息是，你须要为你使用的每个P/Invoke方法手写这些接口。越是调用多的方法，越是多的申明必须手写。这种P/Invoke申明就是告诉CRL如何访问本地方法。这里额外的解释一下为什么每个一个关于P/Invoke的例子里(也包括下面这个)都使用MessageBox:public class PInvokeMsgBox { [ DllImport( "user32.dll" ) ] public static extern int MessageBoxA( int h, string m, string c, int type ); public static int Main() { return MessageBoxA( 0, "P/InvokeTest", "It is using Interop", 0 ); } } 另一个使用P/Invoke的主要缺点是，这并不是设计成面向对象的语言。如果你须要导入C++库，你必须在你的导入申明中指明封装名。假设取代Win32的MessageBox API，你想访问MFC的C++DLL里的另外两个AfxMessageBox 方法。你须要为其中一个方法创建一个P/Invoke申明：?AfxMessageBox@@YGHIII@Z ?AfxMessageBox@@YGHPBDII@Z 这两个申明名是与下面的两个方法匹配的：int AfxMessageBox( LPCTSTR lpszText, UINT nType, UINT nIDHelp ); int AFXAPI AfxMessageBox( UINT nIDPrompt, UINT nType, UINT nIDHelp); 即使是在重写少数几个方法之后，你很快就明白这不是一个高产的方法来提供交互功能。简单的说，使用P/Invoke只能访问C风格的Win32方法(在开发上要开销更多的时间)。最后一种选择就是在Microsoft C++编译器上使用/CLR开关来混合托管的非托管代码。如果你编译你所有的本地代码使用/CLR，你就创建了一个基于MSIL的库，该库使用本地堆来存储所有的数据。这就是说，这样的C++库不能直接被C#调用。你必须在你所熟悉的代码上创建一个托管的C++库，用于在托管和非托管类型之间创建一个桥梁，提供在托管和非托管的堆之间的数据集群支持。这样的C++库包含托管类，这些数据成员是基于托管堆的。这些类同样包含对本地对象的引用：// Declare the managed class: public __gc class ManagedWrapper : public IDisposable { private: NativeType* _pMyClass; public: ManagedWrapper( ) : _pMyClass( new NativeType( ) ) { } // Dispose: virtual void Dispose( ) { delete _pMyClass; _pMyClass = NULL; GC::SuppressFinalize( this ); } ~ManagedWrapper( ) { delete _pMyClass; } // example property: __property System::String* get_Name( ) { return _pMyClass->Name( ); } __property void set_Name( System::String* value ) { char* tmp = new char [ value->Length + 1 ]; for (int i = 0 ; i < value->Length; i++ ) tmp[ i ] = ( char )value->Chars[ i ]; tmp[ i ] = 0; _pMyClass->Name( tmp ); delete [] tmp; } // example method: void DoStuff( ) { _pMyClass->DoStuff( ); } // other methods elided... } 再说一次，这并不是一个像以前使用过的高产的程序开发工具。这只是代码重复，而且整个目的都要在托管和非托管数据之间进行集群数据和thunking。优势是，你可以从你的本地代码中暴露出来的方法和属性上完成控制。而劣势是你必须手写一部份.Net代码以及一部份C++代码。在这两种代码之间转换很容易让你发生错误。你还不能忘记删除非托管对象。托管对象不是你 要负责的。这会让你的开发进度降下来，因为要不断的检测这是否正确。使用 /CLR 开关听上去像是一个神话，但对于所有的交互来说，这并不是一个神弹(magic bullet)。C++里的模板和异常处理与C#是大不相同的。写的很好很高效的C++并不一写能转化成最好的MSIL结构。更重要是，编译C++代码时的 /CLR开关并不做确认。正如我前面所说的，这样的代码是使用本地堆：它访问本地内存。而CLR并不能验证这些代码是安全的。调用这些代码的程序必须确保有安全许可来访问不安全代码。虽然如此，/CLR策略还是最好的一个方法，在.Net中利用已经存在的C++代码(不是COM对象)。你的程序不会招致thunking开销，而为你的C++库现在并不在MSIL中，MSIL不是本地的CPU指令。交互操作是件头疼的事。在你使用交互之间，认真的考虑写本地应用程序。这经常是简单而且很快的。不幸的是，对于很多的开发人员来说，交互是必须的。如果你有已经存在的用其它语言写的COM对象，使用COM交互。如果你有已经存在的C++代码，使用 /CLR 开关并托管C++来提供最好的策略来方法已经存在的本地代码。选择最省时间的一个策略，这可能就是“just thro it out”策略。
当你抛出异常时，你就在应用程序中引入了一个中断事件。而且危机到程序的控制流程。使得期望的行为不能发生。更糟糕的是，你还要把清理工作留给最终写代码捕获了异常的程序员。而当一个异常发生时，如果你可以从你所管理的程序状态中直接捕获，那么你还可以采取一些有效的方法。谢天谢地，C#社区不须要创建自己的异常安全策略，C++社区里的人已经为我们完成了所有的艰巨的工作。以Tom Cargill的文章开头：“异常处理：一种错误的安全感觉，” 而且Herb Sutter，Scott Meyers，Matt Austern，Greg Colvin和Dave Abrahams也在后继写到这些。C++社区里的大量已经成熟的实践可以应用在C#应用程序中。关于异常处理的讨论，一直持续了6年，从1994年到2000年。他们讨论，争论，以及验证很多解决困难问题的方法。我们应该在C#里利用所有这些艰难的工作。Dave Abrahams定义了三种安全异常来保证程序：基本保护，强保证，以及无抛出保证。Herb Sutter在他的Exceptional C++(Addison-Wesley, 2000)一书讨论了这些保证。基本保证状态是指没有资源泄漏，而且所有的对象在你的应用程序抛出异常后是可用的。强异常保证是创建在基本保证之上的，而且添加了一个条件，就是在异常抛出后，程序的状态不发生改变。无抛出保证状态是操作决对不发生失败，也就是从在某个操作后决不会发生异常。强异常保证在从异常中恢复和最简单异常之间最平衡的一个。基本保证一般在是.Net和C#里以默认形式发生。运行环境处理托管内存。只有在一种情况下，你可能会有资源泄漏，那就是在异常抛出时，你的程序占有一个实现了IDisposable接口的资源对象。原则18解释了如何在对面异常时避免资源泄漏。强异常保证状态是指，如果一个操作因为某个异常中断，程序维持原状态不改变。不管操作是否完成，都不修改程序的状态，这里没有折衷。强异常保证的好处是，你可以在捕获异常后更简单的继续执行程序，当然也是在你遵守了强异常保证情况下。任何时候你捕获了一个异常，不管操作意图是否已经发生，它都不应该开始了，而且也不应该做任何修改。这个状态就像是你还没有开始这个操作行为一样。很多我所推荐的方法，可以更简单的帮助你来确保进行强异常保证。你程序使用的数据元素应该存为一个恒定的类型(参见原则6和原则7)。如果你组并这两个原则，对程序状态进行的任何修改都可以在任何可能引发异常的操作完成后简单的发生。常规的原则是让任何数据的修改都遵守下面的原则：1、对可能要修改的数据进行被动式的拷贝。2、在拷贝的数据上完成修改操作。这包括任何可能异常异常的操作。3、把临时的拷贝数据与源数据进行交换。 这个操作决不能发生任何异常。做为一个例子，下面的代码用被动的拷贝方式更新了一个雇员的标题和工资 ：public void PhysicalMove( string title, decimal newPay ) { // Payroll data is a struct: // ctor will throw an exception if fields aren't valid. PayrollData d = new PayrollData( title, newPay, this.payrollData.DateOfHire ); // if d was constructed properly, swap: this.payrollData = d; } 有些时候，这种强保证只是效率很低而不被支持，而且有些时候，你不能支持不发生潜在BUG的强保证。开始的那个也是最简单的那个例子是一个循环构造。当上面的代码在一个循环里，而这个循环里有可能引发程序异常的修改，这时你就面临一个困难的选择：你要么对循环里的所有对象进行拷贝，或者降低异常保证，只对基本保证提供支持。这里没有固定的或者更好的规则，但在托管环境里拷贝堆上分配的对象，并不是像在本地环境上那开销昂贵。在.Net里，大量的时间都花在了内存优化上。我喜欢选择支持强异常保证，即使这意味要拷贝一个大的容器：获得从错误中恢复的能力，比避免拷贝获得小的性能要划算得多。在特殊情况下，不要做无意义的拷贝。如果某个异常在任何情况下都要终止程序，这就没有意义做强异常保证了。我们更关心的是交换引用类型数据会让程序产生错误。考虑这个例子：private DataSet _data; public IListSource MyCollection { get { return _data; } } public void UpdateData( ) { // make the defensive copy: DataSet tmp = _data.Clone( ) as DataSet; using ( SqlConnection myConnection = new SqlConnection( connString )) { myConnection.Open(); SqlDataAdapter ad = new SqlDataAdapter( commandString, myConnection ); // Store data in the copy ad.Fill( tmp ); // it worked, make the swap: _data = tmp; } } 这看上去很不错，使用了被动式的拷贝机制。你创建了一个DataSet的拷贝，然后你就从数据库里攫取数据来填充临时的DataSet。最后，把临时存储交换回来。这看上去很好，如果在取回数据中发生了任何错误，你就相当于没有做任何修改。这只有一个问题：它不工作。MyCollection属性返回的是一个对_data对象的引用(参见原则23)。所有的类的使用客户，在你调用了UpdateData后，还是保持着原原来数据的引用。他们所看到的是旧数据的视图。交换的伎俩在引用类型上不工作，它只能在值类型上工作。因为这是一个常用的操作，对于DataSets有一个特殊的修改方法：使用Merge 方法：private DataSet _data; public IListSource MyCollection { get { return _data; } } public void UpdateData( ) { // make the defensive copy: DataSet tmp = new DataSet( ); using ( SqlConnection myConnection = new SqlConnection( connString )) { myConnection.Open(); SqlDataAdapter ad = new SqlDataAdapter( commandString, myConnection); ad.Fill( tmp ); // it worked, merge: _data.Merge( tmp ); } } 合并修改到当前的DataSet上，就让所有的用户保持可用的引用，而且内部的DataSet内容已经更新。在一般情况下，你不能修正像这样的引用类型交换，然后还想确保用户拥有当前的对象拷贝。交换工作只对值类型有效，如果你遵守原则6，这应该是足够了。最后，也是最严格的，就是无抛出保证。无抛出保证听起来很优美，就像是：一个方法是无抛出保证，如果它保证总是完成任务，而且不会在方法里发生任何异常。在大型程序中，对于所有的常规问题并不是实用的。然而，如果在一个小的范围上，方法必须强制无抛出保证。析构和处理方法就必须保证无异常抛出。在这两种情况下，抛出任何异常会引发更多的问题，还不如做其它的选择。在析构时，抛出异常中止程序就不能进一步的做清理工作了。如果在处理方法中抛出异常，系统现在可能有两个异常在运行系统中。.Net环境丢失前面的一个异常然后抛出一个新的异常。你不能在程序的任何地方捕获初始的异常，它被系统干掉了。这样，你又如何能从你看不见的错误中恢复呢？最后一个要做无抛出保证的地方是在委托对象上。当一个委托目标抛出异常时，在这个多播委托上的其它目标就不能被调用了。对于这个问题的唯一方法就是确保你在委托目标上不抛出任何异常(译注：我不造成这种做法，而且谁又能保证在委托目标上不 出现异常呢？)。让我们再重申一下：委托目标(包括事件句柄)应该不抛出异常。这样做就意味关引发事件的代码不应该参与到强异常保证中。但在这里，我将要修改这一建议。原则21告诉你可以调用委托，因此你可以从一个异常中恢复。想也想得到，并不是每个人都这样做，所以你应该在委托句柄上抛出异常。只在要你在委托上不抛出异常，并不意味着其它人也遵守这一建议，在你自己的委托调用上，不能指望它是无抛出的保证。这主是被动式程序设计：你应该尽可能做原最好，因为其他程序可能做了他们能做的最坏的事。异常列在应用程序的控制流程上引发了一系列的改变，在最糟糕的情况下，任何事情都有可能发生，或者任何事情也有可能不发生。在异常发生时，唯一可以知道哪些事情发生，哪些事情没有发生的方法就是强制强异常保证。当一个操作不管是完成还是没有完成时都不做任何修改。构造和Dispose()以及委托目标是特殊的情况，而且它们应该在不充许任何异常逃出环境的情况下完成任务。最后一句话：小心对引用类型的交换，它可能会引发大量潜在的BUG。
异常是一种的报告错误的机制，它可以在远离错误发生的地方进行处理错误。所有关于错误发生的的信息必须包含在异常对象中。在错误发生的过程中，你可能想把底层的错误转化成详细的应用程序错误，而且不丢失关于错误的任何信息。你须要仔细考虑关于如何在C＃应用程序中创建特殊的异常类。第一步就是要理解什么时候以及为什么要创建新的异常类，以及如何构造继承的异常信息。当开发者使用你的库来写catch语句时，他们是基于特殊的进行时异常在区别为同的行为的。每一个不同的异常类可以有不同的处理要完成：try { Foo( ); Bar( ); } catch( MyFirstApplicationException e1 ) { FixProblem( e1 ); } catch( AnotherApplicationException e2 ) { ReportErrorAndContinue( e2 ); } catch( YetAnotherApplicationException e3 ) { ReportErrorAndShutdown( e3 ); } catch( Exception e ) { ReportGenericError( e ); } finally { CleanupResources( ); } 不同的catch语句可以因为不同的运行时异常而存在。你，做为库的作者，当异常的catch语句要处理不同的事情时，必须创建或者使用不同的异常类。如果不这样，你的用户就只有唯一一个无聊的选择。在任何一个异常抛出时，你可以挂起或者中止应用程序。这当然是最少的工作，但样是不可能从用户那里赢得声望的。或者，他们 可以取得异常，然后试着断定这个错误是否可以修正：try { Foo( ); Bar( ); } catch( Exception e ) { switch( e.TargetSite.Name ) { case "Foo": FixProblem( e ); break; case "Bar": ReportErrorAndContinue( e ); break; // some routine called by Foo or Bar: default: ReportErrorAndShutdown( e ); break; } } finally { CleanupResources( ); } 这远不及使用多个catch语句有吸引力，这是很脆弱的代码：如果只是常规的修改了名字，它就被破坏了。如果你移动了造成错误的函数调用，放到了一个共享的工具函数中，它也被破坏了。在更深一层的堆栈上发生异常，就会使这样的结构变得更脆弱。在深入讨论这一话题前，让我先附带说明两个不能做承诺的事情。首先，异常并不能处理你所遇到的所有异常。这并不是一个稳固指导方法，但我喜欢为错误条件抛出异常，这些错误条件如果不立即处理或者报告，可能会在后期产生更严重的问题。例如，数据库里的数据完整性的错误，就应该生产一个异常。这个问题如果忽略就只会越发严重。而像在写入用户的窗口位置失败时，不太像是在后来会产生一系列的问题。返回一个错误代码来指示失败就足够了。其次，写一个抛出(throw)语句并不意味会在这个时间创建一个新的异常类。我推荐创建更多的异常，而不是只有少数几个常规的自然异常：很从人好像在抛出异常时只对System.Exception情有独钟。可惜只只能提供最小的帮助信息来处理调用代码。相反，考虑创建一些必须的异常类，可以让调用代码明白是什么情况，而且提供了最好的机会来恢复它。再说一遍：实际上要创建不同的异常类的原则，而且唯一原因是让你的用户在写catch语句来处理错误时更简单。查看分析这些错误条件，看哪些可以放一类里，成为一个可以恢复错误的行为，然后创建指定的异常类来处理这些行为。你的应用程序可以从一个文件或者目录丢失的错误中恢复过来吗？它还可以从安全权限不足的情况下恢复吗？网络资源丢失又会怎样呢？对于这种遇到不同的错误，可能要采取不同的恢复机制时，你应该为不同的行为创建新的异常类。因此，现在你应该创建你自己的异常类了。当你创建一个异常类时，你有很多责任要完成。你应该总是从System.ApplicationException类派生你的异常类，而不是System.Exception类。对于这个基类你不用添加太多的功能。对于不同的异常类，它已经具有可以在不同的catch语句中处理的能力了。但也不要从异常类中删除任何东西。ApplicationException 类有四个不同的构造函数：// Default constructor public ApplicationException( ); // Create with a message. public ApplicationException( string ); // Create with a message and an inner exception. public ApplicationException( string, Exception ); // Create from an input stream. protected ApplicationException( SerializationInfo, StreamingContext ); 当你创建一个新的异常类时，你应该创建这个四构造函数。不同的情况调用不同的构造方法来构造异常。你可以委托这个工作给基类来实现：public class MyAssemblyException : ApplicationException { public MyAssemblyException( ) : base( ) { } public MyAssemblyException( string s ) : base( s ) { } public MyAssemblyException( string s, Exception e) : base( s, e ) { } protected MyAssemblyException( SerializationInfo info, StreamingContext cxt ) : base( info, cxt ) { } } 构造函数须要的这个异常参数值得讨论一下。有时候，你所使用的类库之一会发生异常。调用你的库的代码可能会取得最小的关于可能修正行为的信息，当你简单从你使用的异常上传参数时：public double DoSomeWork( ) { // This might throw an exception defined // in the third party library: return ThirdPartyLibrary.ImportantRoutine( ); } 当你创建异常时，你应该提供你自己的库信息。抛出你自己详细的异常，以及包含源异常做为它的内部异常属性。你可以提供你所能提供的最多的额外信息：public double DoSomeWork( ) { try { // This might throw an exception defined // in the third party library: return ThirdPartyLibrary.ImportantRoutine( ); } catch( Exception e ) { string msg = string.Format("Problem with {0} using library", this.ToString( )); throw new DoingSomeWorkException( msg, e ); } } } 这个新的版本会在问题发生的地方创建更多的信息。当你已经创建了一个恰当的ToString()方法时(参见原则5)，你就已经创建了一个可以完整描述问题发生的异常对象。更多的，一个内联异常显示了产生问题的根源：也就是你所使用的第三方库里的一些信息。这一技术叫做异常转化，转化一个底的层异常到更高级的异常，这样可以提供更多的关于错误的内容。你越是创建多的关于错误的额外的信息，就越是容易让它用于诊断，以及可能修正错误。通过创建你自己的异常类，你可能转化底层的问题到详细的异常，该异常包含所详细的应用程序信息，这可以帮助你诊断程序以及尽可能的修正问题。希望你的应用程序不是经常抛出异常，但它会发生。如果你不做任何详细的处理，你的应用程序可能会产生默认的.Net框架异常，而不管是什么错误在你调用的方法里发生。提供更详细的信息将会让你以及你的用户，在实际应用中诊断程序以及可能的修正错误大有帮助。当且仅当对于错误有不同的行为要处理时，你才应该创建不同的异常类。你可以通过提供所有基类支持的构造函数，来创建全功能的异常类。你还可以使用InnerException属性来承载底层错误条件的所有错误信息。
创建二进制的组件时，同时也意味着你要使用迟后绑定和反射来查找你所须要的具有特殊功能代码。反射是一个很有力的工具，而且它让你可以写出可动态配置的软件。使用反射，一个应用程序可以通过添加新的组件来更新功能，而这些组件是在软件最开始发布时没有的。这是有利的。这一伸缩性也带来了一些复杂的问题，而且复杂问题的增加又会增加出现其它问题的可能。当你使用反射时，你是围绕着C#的安全类型。然而，成员调用的参数和返回值是以System.Object类型存在的。你必须在运行时确保这些类型是正确的。简单的说，使用反射可以让创建动态的程序变得很容易，但同时也让程序出现错误变得很容易。通常，简单的思考一下，你就可以通过创建一系列接口集合来最小化或者移除反射，而这些接口集合应该表达你对类型的假设。反射给了你创建类型实例的功能，以及在对象上调用成员方法，以及访问对象上的成员数据。这听上去就跟每天的编程任务是一样的。确实是这样的，对于反射，并没有什么新奇的：它就是动态创建其它的二进制组件。大多数情况下，你并不须要像反射这样的伸缩功能，因为有其它可选的更易维护的方案。让我们从创建一个给定类型的实例开始，你可以经常使用一个类厂来完成同样的任务。考虑下面的代码，它通过使用反射，调用默认的构造函数创建了一个MyType 的实例：// Usage:Create a new object using reflection: Type t = typeof( MyType ); MyType obj = NewInstance( t ) as MyType; // Example factory function, based on Reflection: object NewInstance( Type t ) { // Find the default constructor: ConstructorInfo ci = t.GetConstructor( new Type[ 0 ] ); if ( ci != null ) // Invoke default constructor, and return // the new object. return ci.Invoke( null ); // If it failed, return null. return null; } 代码通过反射检测了类型，而且调用了默认的构造函数来创建了一个对象。如果你须要在运行时创建一个预先不知道任何信息的类型实例，这是唯一的选择。这是一段脆弱的代码，它依懒于默认的构造函数的存在。而且在你移除了MyType类型的默认构造函数时仍然是可以通过编译的。你必须在运行时完成检测，而且捕获任何可能出现的异常。一个完成同样功能的类厂函数，在构造函数被移除时是不能通过编译的：public MyType NewInstance( ) { return new MyType(); } (译注：其实VS.Net会给我们添加默认的构造函数，所以上面的两个方法都是可以编译，而且可以正确运行的。本人做过测试。但如果给构造函数添加访问限制，那么可以让类厂无法构造对象而产生编译时错误。)你应该使用静态的类厂函数来取代依懒于反射的实例创建方法。如果你须要实例对象使用迟后数据绑定，那么应该使用类厂函数，而且使用相关的特性来标记它们(参见原则42)。另一个反射的潜在的用处就是访问类型的成员。你可以使用成员名和类型在运行时来调用实际的函数：// Example usage: Dispatcher.InvokeMethod( AnObject, "MyHelperFunc" ); // Dispatcher Invoke Method: public void InvokeMethod ( object o, string name ) { // Find the member functions with that name. MemberInfo[] myMembers = o.GetType( ).GetMember( name ); foreach( MethodInfo m in myMembers ) { // Make sure the parameter list matches: if ( m.GetParameters( ).Length == 0 ) // Invoke: m.Invoke( o, null ); } } 在上面的代码中，进行是错误被屏避。如果类型名字打错了，这个方法就找不到。就没有方法被调用。这还只是一个简单的例子。要创建一个灵活的InvokeMethod版本，须要从GetParameters()方法上返回的参数列表中，检测所有出现的参数类型。这样的代码是很沉长的，而且很糟糕以至于我根本就不想浪费地方来演示。反射的第三个用处就是访问数据成员。代码和访问成员函数的很类似：// Example usage: object field = Dispatcher.RetrieveField ( AnObject, "MyField" ); // elsewhere in the dispatcher class: public object RetrieveField ( object o, string name ) { // Find the field. FieldInfo myField = o.GetType( ).GetField( name ); if ( myField != null ) return myField.GetValue( o ); else return null; } 和方法调用一样，使用反射来取回一个数据成员，要在一个字段上通过名字来调用类型查询，看它是否与请求的字段名相匹配。如果发现一个，就可以使用FieldInfo 结构来返回值。这个构造在.Net框架里是很常见的。数据绑定就是利用反射来查找这些标记了绑定操作的属性。在这种情况下，数据绑定的动态性质超过了它的开销。(译注：也就是说值得使用反射进行动态绑定。)因此，如果反射是一个如此痛苦的事情，你就须要找一个更好更简单的可选方案。你有三个选择：首先就是使用接口。你可以为任何你所期望的类，结构来定义接口(参见原则19)。这可能会使用更清楚的代码来取代所有的反射代码：IMyInterface foo = obj as IMyInterface; if ( foo != null) { foo.DoWork( ); foo.Msg = "work is done."; } 如果你用标记了特性的类厂函数来合并接口，几乎所有的你所期望于反射的解决方案都变得更简单：public class MyType : IMyInterface { [FactoryFunction] public static IMyInterface CreateInstance( ) { return new MyType( ); } #region IMyInterface public string Msg { get { return _msg; } set { _msg = value; } } public void DoWork( ) { // details elided. } #endregion } 把这段代码与前面的基于反射的方案进行对比。即使这只是简单的例子，但还有在某些弱类型上使用所有的反射API时有精彩之处：返回类型已经是类型化的对象。而在反射上，如果你想取得正确的类型，你须要强制转换。这一操作可能失败，而且在继承上有危险。而在使用接口时，编译器提供的强类型检测显得更清楚而且更易维护。反射应该只在某些调用目标不能清楚的用接口表示时才使用。.Net的数据绑定是在类型的任何公共属性上可以工作，把它限制到定义的接口上可能会很大程度上限制它的使用。菜单句柄的例子充许任何函数(不管是实例的还是静态的)来实现命令句柄，使用一个接口同样会限制这些功能只能是实例方法。FxCop 和NUnit (参见原则48)都扩展了反射的使用，它们使用反射，是因为它们遇到的的现实的问题是最好用它来处理的。FxCopy 检测所有的代码来评估它们是否与已经的原则矛盾。这须要使用反射。NUnit 必须调用你编译的测试代码。它使用反射来断定哪些你已经写的代码要进行单元测试。对于你可能要写的测试代码，可能是一个方法集合，但接口是不能表达它们的。NUnit使用特性来发现测试以及测试案例来让它的工作更简单(参见原则42)。当你可以使用接口策划出你所期望调用的方法和属性时，你就可以拥有一个更清楚，更容易维护的系统。反射是一个在数据以后绑定上功能强大的工具。.Net框架使用它实现对Windows控件和Web控件的数据绑定。然而，很多常规情况下很少用，而是使用类厂，委托，以及接口来创建代码，这可以产生出更容易维护的系统。
当你创建了一个与反射相关的系统时，你应该为你自己的类型，方法，以及属性定义一些自己的特性，这样可以让它们更容易的被访问。自定义的特性标示了你想让这些方法在运行时如何被使用。特性可以测试一些目标对象上的属性。测试这些属性可以最小化因为反射时可能而产生的类型错误。假设你须要创建一个机制，用于在运行时的软件上添加一个菜单条目到一个命令句柄上。这个须要很简单：放一个程序集到目录里，然后程序可以自己发现关于它的一些新菜单条目以及新的菜单命令。这是利用反射可以完成的最好的工作之一：你的主程序须要与一些还没有编写的程序集进行交互。这个新的插件同样不用描述某个集合的功能，因为这可以很好的用接口来完成编码。让我们为创建一个框架的插件来开始动手写代码吧。你须要通过Assembly.LoadFrom() 函数来加载一个程序，而且要找到这个可能提供菜单句柄的类型。然后须要创建这个类型的一个实例对象。接着还要找到这个实例对象上可以与菜单命令事件句柄的申明相匹配的方法。完成这些任务之后，你还须要计算在菜单的什么地方添加文字，以及什么文字。特性让所有的这些任务变得很简单。通过用自己定义的特性来标记不同的类以及事件句柄，你可以很简单的完成这些任务：发现并安装这些潜在的命令句柄。你可以使用特性与反射来协作，最小化一些在原则43中描述的危险事情。第一个任务就是写代码，发现以及加载插件程序集。假设这个插件在主执行程序所在目录的子目录中。查找和加载这个程序集的代码很简单：// Find all the assemblies in the Add-ins directory: string AddInsDir = string.Format( "{0}/Addins", Application.StartupPath ); string[] assemblies = Directory.GetFiles( AddInsDir, "*.dll" ); foreach ( string assemblyFile in assemblies ) { Assembly asm = Assembly.LoadFrom( assemblyFile ); // Find and install command handlers from the assembly. } 接下来，你须要把上面最后一行的注释替换成代码，这些代码要查找那些实现了命令句柄的类并且要安装这些句柄。加载完全程序集之后，你就可以使用反射来查找程序集上所有暴露出来的类型，使用特性来标识出哪些暴露出来的类型包含命令句柄，以及哪些是命令句柄的方法。下面是一个添加了特性的类，即标记了命令句柄类型：// Define the Command Handler Custom Attribute: [AttributeUsage( AttributeTargets.Class )] public class CommandHandlerAttribute : Attribute { public CommandHandlerAttribute( ) { } } 这个特性就是你须要为每个命令标记的所有代码。总是用AttributeUsage 特性标记一个特性类，这就是告诉其它程序以及编译器，在哪些地方这个特性可以使用。前面这个例子表示CommandHandlerAttribute只能在类上使用，它不能应用在其它语言的元素上。你可以调用GetCustomAttributes来断定某个类是否具有CommandHandlerAttribute特性。只有具有该特性的类型才是插件的候选类型 ：// Find all the assemblies in the Add-ins directory: string AddInsDir = string.Format( "{0}/Addins", Application.StartupPath); string[] assemblies = Directory.GetFiles( AddInsDir, "*.dll" ); foreach ( string assemblyFile in assemblies ) { Assembly asm = Assembly.LoadFrom( assemblyFile ); // Find and install command handlers from the assembly. foreach( System.Type t in asm.GetExportedTypes( )) { if (t.GetCustomAttributes( typeof( CommandHandlerAttribute ), false ).Length > 0 ) { // Found the command handler attribute on this type. // This type implements a command handler. // configure and add it. } // Else, not a command handler. Skip it. } } 现在，让我们添加另一个新的特性来查找命令句柄。一个类型应该可以很简单的实现好几个命令句柄，所以你可以定义新的特性，让插件的作者可以把它添加到命令句柄上。这个特性会包含一参数，这些参数用于定义新的菜单命令应该放在什么地方。每一个事件句柄处理一个特殊的命令，而这个命令应该在菜单的某个特殊地方。为了标记一个命令句柄，你要定义一个特性，用于标记一个属性，让它成为一个命令句柄，并且申明菜单上的文字以及父菜单文字。DynamicCommand特性要用两个参数来构造：菜单命令文字以及父菜单的文字。这个特性类还包含一个构造函数，这个构造函数用于为菜单初始化两个字符串。这些内容同样可以使用可读可写的属性：[AttributeUsage( AttributeTargets.Property ) ] public class DynamicMenuAttribute : System.Attribute { private string _menuText; private string _parentText; public DynamicMenuAttribute( string CommandText, string ParentText ) { _menuText = CommandText; _parentText = ParentText; } public string MenuText { get { return _menuText; } set { _menuText = value; } } public string ParentText { get { return _parentText; } set { _parentText = value; } } } 这个特性类已经做了标记，这样它只能被应用到属性上。而命令句柄必须在类中以属性暴露出来，用于提供给命令句柄来访问。使用这一技术，可以让程序在启动的时候查找和添加命令句柄的代码变得很简单。现在你创建了这一类型的一个对象：查找命令句柄，以及添加它们到新的菜单项中。你可以把特性和反射组合起来使用，用于查找和使用命令句柄属性，对对象进行推测：// Expanded from the first code sample: // Find the types in the assembly foreach( Type t in asm.GetExportedTypes( ) ) { if (t.GetCustomAttributes( typeof( CommandHandlerAttribute ), false).Length > 0 ) { // Found a command handler type: ConstructorInfo ci = t.GetConstructor( new Type[0] ); if ( ci == null ) // No default ctor continue; object obj = ci.Invoke( null ); PropertyInfo [] pi = t.GetProperties( ); // Find the properties that are command // handlers foreach( PropertyInfo p in pi ) { string menuTxt = ""; string parentTxt = ""; object [] attrs = p.GetCustomAttributes( typeof ( DynamicMenuAttribute ), false ); foreach ( Attribute at in attrs ) { DynamicMenuAttribute dym = at as DynamicMenuAttribute; if ( dym != null ) { // This is a command handler. menuTxt = dym.MenuText; parentTxt = dym.ParentText; MethodInfo mi = p.GetGetMethod(); EventHandler h = mi.Invoke( obj, null ) as EventHandler; UpdateMenu( parentTxt, menuTxt, h ); } } } } } private void UpdateMenu( string parentTxt, string txt, EventHandler cmdHandler ) { MenuItem menuItemDynamic = new MenuItem(); menuItemDynamic.Index = 0; menuItemDynamic.Text = txt; menuItemDynamic.Click += cmdHandler; //Find the parent menu item. foreach ( MenuItem parent in mainMenu.MenuItems ) { if ( parent.Text == parentTxt ) { parent.MenuItems.Add( menuItemDynamic ); return; } } // Existing parent not found: MenuItem newDropDown = new MenuItem(); newDropDown.Text = parentTxt; mainMenu.MenuItems.Add( newDropDown ); newDropDown.MenuItems.Add( menuItemDynamic ); } 现在你将要创建一个命令句柄的示例。首先，你要用CommandHandler 特性标记类型，正如你所看到的，我们习惯性的在附加特性到项目上时，在名字上省略Attribute：Now you'll build a sample command handler. First, you tag the type with the CommandHandler attribute. As you see here, it is customary to omit Attribute from the name when attaching an attribute to an item:[CommandHandler] public class CmdHandler { // Implementation coming soon. } 在CmdHandler 类里面，你要添加一个属性来取回命令句柄。这个属性应该用DynamicMenu 特性来标记：[DynamicMenu( "Test Command", "Parent Menu" )] public EventHandler CmdFunc { get { if ( theCmdHandler == null ) theCmdHandler = new System.EventHandler (this.DynamicCommandHandler); return theCmdHandler; } } private void DynamicCommandHandler( object sender, EventArgs args ) { // Contents elided. } 就是这了。这个例子演示了你应该如何使用特性来简化使用反射的程序设计习惯。你可以用一个特性来标记每个类型，让它提供一个动态的命令句柄。当你动态的载入这个程序集时，可以更简单的发现这个菜单命令句柄。通过应用AttributeTargets (另一个特性)，你可以限制动态命令句柄应用在什么地方。这让从一个动态加载的程序集上查找类型的困难任务变得很简单：你确定从很大程度上减少了使用错误类型的可能。这还不是简单的代码，但比起不用特性，还算是不错的。特性可以申明运行的意图。通过使用特性来标记一个元素，可以在运行时指示它的用处以及简化查找这个元素的工作。如何没有特性，你须要定义一些命名转化，用于在运行时来查找类型以及元素。任何命名转化都会是发生错误的起源。通过使用特性来标记你的意图，就把大量的责任从开发者身上移到了编译器身上。特性可以是只能放置在某一特定语言元素上的，特性同样也是可以加载语法和语义信息的。你可以使用反射来创建动态的代码，这些代码可以在实际运行中进行配置。设计和实现特性类，可以强制开发者为申明一些类型，方法，以及属性，这些都是可以被动态使用的，而且减少潜在的运行时错误。也就是说，让你增加了创建让用户满足的应用程序的机会。
因为两个原则，把DataSet的名声搞的不好。首先就是使用XML序列化的DataSet与其它的非.Net代码进行交互时不方便。如果在Web服务的API中使用DataSet时，在与其它没有使用.Net框架的系统进行交互时会相当困难。其次，它是一个很一般的容器。你可以通过欺骗.Net框架里的一些安全类型来错误DataSet。但在现代软件系统中，DataSet还可以解决很多常规的问题。如果你明白它的优势，避免它的缺点，你就可以扩展这个类型了。DataSet类设计出来是为了离线使用一些存储在相关数据库里的数据。你已经知道它是用来存储DataTable的，而DataTable就是一个与数据库里的结构在行和列上进行匹配的内存表。或许你已经看到过一些关于DataSet支持在内部的表中建立关系的例子。甚至还有可能，你已经见过在DataSet里验证它所包含的数据，进行数据约束的例子。但不仅仅是这些，DataSet还支持AcceptChanges 和RejectChanges 方法来进行事务处理，而且它们可以做为DiffGrams存储，也就是包含曾经修改过的数据。多个DataSet还可以通过合并成为一个常规的存储库。DataSet还支持视图，这就是说你可以通过标准的查询来检测数据里的部份内容。而且视图是可以建立在多个表上的。然而，有些人想开发自己的存储结构，而不用DataSet。因DataSet是一个太一般的容器，这会在性能上有所损失。一个DataSet并不是一个强类型的存储容器，其实存储在里面的对象是一个字典。而且在里的表中的列也是字典。存储在里的元素都是以System.Object的引用形式存在。这使得我们要这样写代码：int val = (int)MyDataSet.Tables["table1"]. Rows[0]["total"]; 以C#强类型的观点来看，这样的结构是很麻烦的。如果你错误使用table1 或者total的类型，你就会得到一个运行时错误。访问里面的数据元素要进行强制转化。而这样的麻烦事情是与你访问里面的元素的次数成正比的，与其这样，我们还真想要一个类型化的解决方法。那就让我们来试着写一个DataSet吧，基于这一点，我们想要的是：int val = MyDataSet.table1.Rows[ 0 ].total; 当你看明白了类型化的DataSet内部的C#实现时，就会知道这是完美的。它封装了已经存在的DataSet，而且在弱类型的访问基础上添加了强类型访问。你的用户还是可以用弱类型API。但这并不是最好的。与它同时存在的，我会告诉你我们放弃了多少东西。我会告诉你DataSet类里面的一些功能是如何实现的，也就是在我们自己创建的自定义集合中要使用的。你可能会觉得这很困难，或者你觉得我们根本用上不同DataSet的所有功能，所以，代码并不会很长。OK，很好，我会写很长的代码。假设你要创建一个集合，用于存储地址。每一个独立的元素必须支持数据绑定，所以你我创建一个具有下面公共属性的结构：public struct AddressRecord { private string _street; public string Street { get { return _street; } set { _street = value; } } private string _city; public string City { get { return _city; } set { _city = value; } } private string _state; public string State { get { return _state; } set { _state = value; } } private string _zip; public string Zip { get { return _zip; } set { _zip = value; } } } 下面，你要创建这个集合。因为我们要类型安全的集合，所以我们要从CollectionsBase派生：public class AddressList : CollectionBase { } CollectionBase 支持IList 接口，所以你可以使用它来进行数据绑定。现在，你就发现了你的第一个问题：如果地址为空，你的所有数据绑定行就失败了。而这在DataSet里是不会发生的。数据绑定是由基于反射的迟后绑定代码组成的。控件使用反射来加载列表里的第一个元素，然后使用反射来决定它的类型以及这个类型上的所有成员属性。这就是为什么DataGrid可以知道什么列要添加。它会在集合中的第一个元素上发现所有的公共属性，然后显示他们。当集合为空时，这就不能工作了。你有两种可能来解决这个问题。第一个方法有点丑，但是一个简单的方法：那就是不充许有空列表存在。第二个好一些，但要花点时间：那就是实现ITypedList 接口。ITypedList 接口提供了两个方法来描述集合中的类型。GetListName 返回一个可读的字符串来描述这个列表。GetItemProperties 则返回PropertyDescriptors 列表，这是用于描述每个属性的，它要格式化在表格里的：public class AddressList : CollectionBase { public string GetListName(PropertyDescriptor[] listAccessors) { return "AddressList"; } public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { Type t = typeof(AddressRecord); return TypeDescriptor.GetProperties(t); } } 这稍微好一点了，现在你你已经有一个集合可以支持简单的数据绑定了。尽管，你失去了很多功能。下一步就是要实现数据对事务的支持。如果你使用过DataSet，你的用户可以通过按Esc键来取消DataGrid中一行上所有的修改。例如，一个用户可能输入了错误的城市，按了Esc，这时就要原来的值恢复过来。DataGrid同样还支持错误提示。你可以添加一个ColumnChanged 事件来处理实际列上的验证原则。例如，州的区号必须是两个字母的缩写。使用框架里的DataSet，可以这样写代码：ds.Tables["Addresses"].ColumnChanged +=new DataColumnChangeEventHandler(ds_ColumnChanged); private void ds_ColumnChanged(object sender, DataColumnChangeEventArgs e) { if (e.Column.ColumnName == "State") { string newVal = e.ProposedValue.ToString(); if (newVal.Length != 2) { e.Row.SetColumnError(e.Column, "State abbreviation must be two letters"); e.Row.RowError = "Error on State"; } else { e.Row.SetColumnError(e.Column, ""); e.Row.RowError = ""; } } } 为了在我们自己定义的集合上也实现这样的概念，我们很要做点工作。你要修改你的AddressRecord 结构来支持两个新的接口，IEditableObject 和IDataErrorInfo。IEditableObject 为你的类型提供了对事务的支持。IDataErrorInfo 提供了常规的错误处理。为了支持事务，你必须修改你的数据存储来提供你自己的回滚功能。你可能在多个列上有错误，因此你的存储必须包含一个包含了每个列的错误集合。这是一个为AddressRecord做的更新的列表：public class AddressRecord : IEditableObject, IDataErrorInfo { private struct AddressRecordData { public string street; public string city; public string state; public string zip; } private AddressRecordData permanentRecord; private AddressRecordData tempRecord; private bool _inEdit = false; private IList _container; private Hashtable errors = new Hashtable(); public AddressRecord(AddressList container) { _container = container; } public string Street { get { return (_inEdit) ? tempRecord.street : permanentRecord.street; } set { if ( value.Length == 0 ) errors[ "Street" ] = "Street cannot be empty"; else { errors.Remove( "Street" ); } if ( _inEdit ) tempRecord.street = value; else { permanentRecord.street = value; int index = _container.IndexOf( this ); _container[ index ] = this; } } } public string City { get { return (_inEdit) ? tempRecord.city : permanentRecord.city; } set { if (value.Length == 0) errors["City"] = "City cannot be empty"; else { errors.Remove("City"); } if (_inEdit) tempRecord.city = value; else { permanentRecord.city = value; int index = _container.IndexOf(this); _container[index] = this; } } } public string State { get { return (_inEdit) ? tempRecord.state : permanentRecord.state; } set { if ( value.Length == 0 ) errors[ "State" ] = "City cannot be empty"; else { errors.Remove( "State" ); } if ( _inEdit ) tempRecord.state = value; else { permanentRecord.state = value; int index = _container.IndexOf( this ); _container[ index ] = this; } } } public string Zip { get { return ( _inEdit ) ? tempRecord.zip : permanentRecord.zip; } set { if ( value.Length == 0 ) errors["Zip"] = "Zip cannot be empty"; else { errors.Remove ( "Zip" ); } if ( _inEdit ) tempRecord.zip = value; else { permanentRecord.zip = value; int index = _container.IndexOf( this ); _container[ index ] = this; } } } public void BeginEdit( ) { if ( ( ! _inEdit ) && ( errors.Count == 0 ) ) tempRecord = permanentRecord; _inEdit = true; } public void EndEdit( ) { // Can't end editing if there are errors: if ( errors.Count > 0 ) return; if ( _inEdit ) permanentRecord = tempRecord; _inEdit = false; } public void CancelEdit( ) { errors.Clear( ); _inEdit = false; } public string this[string columnName] { get { string val = errors[ columnName ] as string; if ( val != null ) return val; else return null; } } public string Error { get { if ( errors.Count > 0 ) { System.Text.StringBuilder errString = new System.Text.StringBuilder(); foreach ( string s in errors.Keys ) { errString.Append( s ); errString.Append( ", " ); } errString.Append( "Have errors" ); return errString.ToString( ); } else return ""; } } } 花了几页的代码来支持一些已经在DataSet里实现的了的功能。实际上，这还不能像DataSet那样恰当的工作。例如，交互式的添加一个新记录到集合中，以及支持事务所要求的BeginEdit, CancelEdit, 和EndEdit等。 你要在CancelEdit 调用时检测一个新的对象而不是一个已经修改了的对象。CancelEdit 必须从集合上移除这个新的对象，该对象应该是上次调用BeginEdit时创建的。对于AddressRecord 来说，还有很多修改要完成，而且一对事件还要添加到AddressList 类上。最后，就是这个IBindingList接口。这个接口至少包含了20个方法和属性，用于控件查询列表上的功能描述。你必须为只读列表实现IBindingList 或者交互排序，或者支持搜索。在你取得内容之前就陷于层次关系和导航关系中了。我也不准备为上面所有的代码添加任何例子了。几页过后，再问问你自己，还准备创建你自己的特殊集合吗？或者你想使用一个DataSet吗？除非你的集合是一个基于某些算法，对性能要求严格的集合，或者必须有轻便的格式，就要使用自己的DataSet，特别是类型化的DataSet。这将花去你大量的时间，是的，你可以争辩说DataSet并不是一个基于面向对象设计的最好的例子。类型化的DataSet甚至会破坏更多的规则。但，使用它所产生的代码开发效率，比起自己手写更优美的代码所花的时间，这只是其中一小部份。
“哪种集合是最好的？”答案是：“视情况而定。” 不同的集合有不同的性能，而且在不同的行为上有不同的优化。.Net框架支持很多类似的集合：链表，数组，队列，栈，以及其它的一些集合。C#支持多维的数组，它的性能与一维的数组和锯齿数组都有所不同。.Net框架同样包含了很多特殊的集合，在你创建你自己的集合类之前，请仔细参阅这些集合。你可以发现很多集合很快，因为所有的集合都实现了ICollection接口。在说明文档中列出了所有实现了ICollection接口的集合，你将近有20多个集合类可用。为了选择适合你使用的集合，你须要考虑在该集合上使用的操作。为了创建一个有伸缩性的程序，你须要使用一些实现了接口的集合类，这样当你发现某个假设的集合不正确时，你可以用其它不同的集合来代替它(参见原则19)。.Net框架有三种不同类型的集合：数组，类似数组的集合，以及基于散列值的集合。数组是最简单的也是一般情况下最快的集合，因此我们就从它开始。这也是你经常要使用到的集合类。你的第一选择应该经常是System.Array类，确切的说，应该是一个数组类型的类。 选择数组类的首要原因，也是最重要的原因是数组是类型安全的。所有其它集合都是存储的System.Object引用，直到C#2.0才引入了范型(参见原则49)。当你申明一个数组时，编译器为你的类型创建一个特殊的System.Array派生类。例如：这样创建了申明并创建了一个整型的数组：private int [] _numbers = new int[100]; 这个数组存储的是整型，而不是System.object的引用。这很重要，因为你在一个数组上添加，访问，删除值类型时，可以避免装箱与拆箱操作的性能损失(参见原则17)。上面的初始化操作创建了一个一维的数组，里面存放了100个整数。所有被数组占用的内存都被清0了。值类型数组都是0，而引用数组都是null。所有在数组里的元素可以用索引访问：int j = _numbers[ 50 ]; 另外，访问数组时还可以用foreach迭代，或者使用枚举器：foreach (int i in _numbers) Console.WriteLine(i.ToString()); // or: IEnumerator it = _numbers.GetEnumerator(); while(it.MoveNext()) { int i = (int) it.Current; Console.WriteLine(i.ToString()); } 如果准备存储单一次序的对象，你应该使用数组。但实际上你的数据结构往往比这复杂的多。这很快诱使我们回到了C风格上的锯齿数组，那就是一个数组里存储另一个数组。有些时候这确实是你想要的，每个外层元素都是内层的一个数组：public class MyClass { // Declare a jagged array: private int[] [] _jagged; public MyClass() { // Create the outer array: _jagged = new int[5][]; // Create each inner array: _jagged[0] = new int[5]; _jagged[1] = new int[10]; _jagged[2] = new int[12]; _jagged[3] = new int[7]; _jagged[4] = new int[23]; } } 每个内层的一维数组可以有同不的大小。在需要不同大小的数组时可以使用锯齿数据。使用锯齿数组的缺点是在列方面上的访问是底效的。检查一个每行上有三个列的锯齿数组时，每次访问都要做两个检测 。在行0列3上的元素和在行1列3的元素没有任何关系。只有多维数组才可在以列方面上的访问有一定的效率。过去，C和C++程序使用二维或多维数组时是把它们映射到一个一维数组上。对于老式的C和C++程序员，这样的标记应该很清楚：double num = MyArray[ i * rowLength + j ]; 这个世界上的其它人可能更喜欢这样：double num = MyArray[i, j]; 但，C和C++其实并不支持多维数组。C#可以，而且对于多维数组的语法：当你创建一个真实的多维数组时，对于编译器和你来说意义都是很清楚的。创建多维数组时只用在熟悉的一维数组标记上扩展一下就行了：private int[,] _multi = new int[10, 10]; 前面的申明创建了一个二维数组，而且是10X10的100个元素。在多维数组上的每一维的长度总是一致的。编译器利用这一特性，可以创建更高效的代码。初始化锯齿数组时须要多次使用初始语句。而在我前面的例子里(译注：指这里：private int[] [] _jagged;)，你须要5个语句。更大的数组或者更多维的数组须要更多的初始代码，而且你要手动编写。然而 ，多维数组在初始化时只要指定多少维就行了。此外，多维数组在初始化元素时更有效。以于值类型数组在初始化时是直接在有效的数组索引上包含该值，所有的内容就是0。而引用类型的数组则是null。数组的数组在内层上都包含null引用。访问多维数组时比访问锯齿数组要快得多，特殊是在列或者对角线上访问时。编译器在数组的每个维上是使用的指针算法。锯齿数组则要为每个一维数组查找正确的(指针引用)值。多维数组可以当成数组在很多情况下使用。假设你创建一个棋盘游戏，你可能须要在一个格子上创建一个64个方格的棋盘：private Square[ , ] _theBoard = new Square[ 8, 8 ]; 这个初始化创建了数组来存储方格，假设这些方格是引用类型，这些方格自己还没有创建，而且每个元素还是null。为了初始化每个元素，你还要检测数组上的每一维元素：for(int i = 0; i < _theBoard.GetLength( 0 ); i++) for(int j = 0; j < _theBoard.GetLength( 1 ); j++) _theBoard[ i, j ] = new Square(); 你你还有更灵活的方法来访问多维数组，你可以给定个别的元素索引来访问该元素：Square sq = _theBoard[ 4, 4 ]; 如果你要迭代整个集合，你还可以使用迭代器：foreach(Square sq in _theBoard ) sq.PaintSquare(); 与锯齿数组相比，你可能要这样写：foreach(Square[] row in _theBoard) foreach(Square sq in row) sq.PaintSquare(); 锯齿数组里的每一个新的维引用了另一个foreach语句。然而，对于一个多维数组，每一个foreach语句都要产生代码来检测数组每个维上的界线。foreach语句在每个维上生成特殊的代码来迭代数组。foreach循环生成的代码与你这样手写是一样的：for (int i = _theBoard.GetLowerBound(0); i <= _theBoard.GetUpperBound(0); i++) for(int j = _theBoard.GetLowerBound(1); j <= _theBoard.GetUpperBound(1); j++) _theBoard[i, j].PaintSquare(); 考虑在内层循环上所有对GetLowerBound 和GetUpperBound 的调用，这看上去是认低效的，但这确实是很高效的结构。JIT编译器对数组类是很了解的，它们会缓存数组界线，而且可以识别内层的迭代界线从而省略它们(参见原则11)。数组类的两个主要的不利因素可能会让你考虑使用.Net框架里的其它集合类。第一个影响就是改变数组的大小：数组不能动态的改变大小。如果你须要修改任意数组及任意维数的大小，你必须创建一个新的数组，然后拷贝所有已经存在的元素到新的数组里。改变大小要花时间：因为新的数组要分配，而且所有的元素要拷贝到新的数组里。尽管拷贝和移动在托管堆上并不是像使用C和C++的年代那样开销昂贵，但还是很花时间。更重要的是，它会让数据使用陈旧。考虑下面的代码：private string [] _cities = new string[100]; public void SetDataSources() { myListBox.DataSource = _cities; } public void AddCity(string CityName) { String[] tmp = new string[_cities.Length + 1]; _cities.CopyTo(tmp, 0); tmp[_cities.Length] = CityName; _cities = tmp; // swap the storage. } 即使在调用AddCity 之后，列表框的数据源还是使用拷贝前的旧数据。你新添加的城市根本没有在列表框中显示。ArrayList类是在数组上的一个高度抽象类。ArrayList集合混合了一维数组和链表的语义。你可以在ArrayList使用迭代，而且你可以调整它的大小。ArrayList基本上把所有责任都委托给了它所包含了数组上，这就是说ArrayList类与数组类在性能上很接近。它的最大好处就是在你不清楚你的集合具体要多大时，ArrayList类要比数组类好用得多。ArrayList可以随时扩展和收缩。但你还是要在移动和拷贝元素上有性能损失，但这些算法的代码已经写好了而且经过了测试。因为内部存储的数组已经封装在ArrayList对象内，因此陈旧数据的情况就不存在了：用户的指针是指向ArrayList对象而不是内部的数组。.Net框架里的ArrayList集合有点类似C++中标准库里的向量类。队列和栈类在System.Array类上提供了特殊的接口。这些特殊的接口是自定义的先进先出的队列接口和后进先出的栈接口。时刻记住，这些集合的内部存储都是基于数组的。在修改集合的大小时同样会得到性能的损失。(译注：对于这样的一些集合，在初始化时都有一个大小的参数，应该尽可能的给定集合的容量大小，这里的大小并不是一次性申明和使用的，而是集合中的元素小于这个大小时，不会有移动和拷贝元素的性能损失。因此合理的选择这些集合的容量大小是很关键的。队列的默认大小是32，而栈的默认大小是10，ArrayList默认是0。而它们的构造函数都有一个重载版本可以指定容量。).Net中的集合没有包含链表(linked list)结构。垃圾回收器的高效使得列表(list)结构在实际使用是占用时间最小，因此也让它成为了最佳选择。如果你确实须要链表的行为，你有两个选择。如果你因为希望经常添加和删除里面的元素而选择了列表，那么使用null引用的字典类。简单的存储关键字，你就可以使用ListDictionary 类了，而且它用键/值对应方式实现了单向链表。或者你可以选择HybridDictionary类，这是一个为小集合使用了ListDictionary的类，而且对于大集合会转化到HashTable上来。这些集合以及很多其它内容都在 System.Collections.Specialized 名字空间里。然而，如果因为用户想控制次序而你想使用列表结构，你可以使用ArrayList 集合。ArrayList 可以在任何位置上执行插入操作，即使它的内部是使用的数组存储方式。另外两个类支持基于字典的集合：SortedList 和Hashtable。它们都是包含键/值对应的。SortedList以键进行排序，而Hashtable则没有。Hashtable在给定的键上进行查找时更快，但SortedList提供了基于键的有序迭代。Hashtable 通过键的散列值进行查找。如果散列值是高效的，它的查找时间是一个常量，O(1)。有序列表是使用的折半查找算法来查找关键值。这中一个对数的算法操作 ：O(ln n)。最后，就是BitArray类。正如它的名字所说的，它存储位数值。BitArray是以整数的数组来存储数据的。每个整型存储一个32位的二进制值。这让BitArray类是压缩的，但它还是会降低性能。BitArray 上的每个get和set操作要在存储的整数上完成位操作，这要查找另外的31个位。BitArray 包含的一些方法在很多值类型上实现了立即的Boolean操作：OR, XOR, AND, 和NOT。这些方法用BitArray做为参数，而且BitArray可以用于快速的多位mask操作，BitArray 是一个为位操作进行优化了的容器；经常使用mask操作时，要存储这些位标记的数据时应该使用它。但不要用它灰替换一般用图和Boolean值数组。对于数组类的概念而言，在C#的1.x发布版中，没有一个集合类是强类型的。它们都是存储的对象引用。C#的范型将会在所有这些拓扑上包含新的版本。这会是一个最好的方法来创建类型安全的集合。同时，目前的System.Collections 名字空间中包含了抽象的基类，利用这些基类，你可以在类型不安全的集合上创建你自己的类型安全的接口：CollectionBase 和ReadOnlyCollectionBase 提供了列表和向量结构的基类。DictionaryBase 提供了键/值对应的基类。DictionaryBase类是建立在Hashtable上的，它的性能与Hashtable是一致的。译注：在C#1.x中，我们其实可以合建类型安全的集合，当然这只是利用编译器一些特性，例如： public class MyObject { } public class MyObjectCollection : ArrayList { [ObsoleteAttribute("Please use specify object.",true)] public override int Add(object value) { return base.Add (value); } public int Add(MyObject value) { return base.Add (value); } new public MyObject this[int index] { get { return base[index] as MyObject; } set { base[index] = value; } } } 这样就可以简单的验证集合的类型了，当然，本质上还是类型不安全的，这只是一个折衷的办法。任何时候，你的类型包含集合时，你可能想要把这些集合在你的类上暴露给你的用户。你有两个方法：使用索引或者使用IEnumerable接口。记住，在这一原则的一开始，我就说明过，你可以直接使用[]标记来访问一个数组里的元素，而且你可以用foreach来迭代数组里的任何元素。你可以为你的类创建多维的索引，以C++里你可能须要重载[]操作。而在C#里，你可以创建多维索引：public int this [int x, int y] { get { return ComputeValue(x, y); } } 添加对索引器的支持就是说你的类型包含一个集合。这也就是说你应该支持IEnumerable 接口。IEnumerable 提供了标准的机制来迭代所有的集合元素：public interface IEnumerable { IEnumerator GetEnumerator( ); } GetEnumerator 方法返回一个实现了IEnumerator 接口的对象。IEnumerator 接口支持集合的历遍：public interface IEnumerator { object Current{ get; } bool MoveNext(); void Reset(); } 另外，在使用IEnumerable 接口时，如果你的类型模型是数组，你应该考虑IList 和ICollection 接口。如果你的类型模型是字典，你应该考虑实现IDictionary 接口。你可以自己创建对这些功能强大的接口的实现，而且你可能要花上更多的几页代码来解释如何实现。但这里有一个简单的解决方案：在你创建自己的特殊集合类时从CollectionBase 或者DictionaryBase 派生你的类。让我们回顾一下今天的内容，最好的集合取决于你对操作的实现，以及应用程序对空间和时间的目标要求。在大多数情况下，数组提供了最高效的容器。C#里扩展的多维数组，可以简单的实现多维的结构，而且没有性能的损失。当我们的应用程序须要对元素进行更灵活的添加和移动操作时，使用众多集合中更活越的一个就行了。最后，当你创建自己的集合模型类时，不论什么时候都实现索引器和IEnumerable接口。