技术掘金 - 2015年1月
type: channel
link: http://blog.guqiankun.com/2015/01/
description: 技术掘金,专注于Web互联网开发,分享技术,致力于打造好的技术博客
items: Effective C# 原则49：为C#2.0做好准备
items: Effective C# 原则48：了解更多的工具和资源
items: Effective C# 原则47：选择安全的代码
items: Effecitve C# 原则46：最小化与其它非托管代码的交互
items: Effective C# 原则45：选择强异常来保护程序
items: Effective C# 原则44：创建应用程序特定的异常类
items: Effective C# 原则43：请勿滥用反射
items: Effective C# 原则42：使用特性进行简单的反射
items: Effective C# 原则41：选择DataSet而不是自定义的数据结构
items: Effective C# 原则40：根据需求选择集合

Effective C# 原则49：为C#2.0做好准备
type: item
date: 2015-01-31T20:47:00+08:00
description: 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 >(); …

Effective C# 原则48：了解更多的工具和资源
type: item
date: 2015-01-30T20:44:00+08:00
description: 对于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.co…

Effective C# 原则47：选择安全的代码
type: item
date: 2015-01-29T20:43:00+08:00
description: .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…

Effecitve C# 原则46：最小化与其它非托管代码的交互
type: item
date: 2015-01-28T20:40:00+08:00
description: 在开发设计.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类型的一维数组也是blittab…

Effective C# 原则45：选择强异常来保护程序
type: item
date: 2015-01-27T20:35:00+08:00
description: 当你抛出异常时，你就在应用程序中引入了一个中断事件。而且危机到程序的控制流程。使得期望的行为不能发生。更糟糕的是，你还要把清理工作留给最终写代码捕获了异常的程序员。而当一个异常发生时，如果你可以从你所管理的程序状态中直接捕获，那么你还可以采取一些有效的方法。谢天谢地，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.…

Effective C# 原则44：创建应用程序特定的异常类
type: item
date: 2015-01-26T20:33:00+08:00
description: 异常是一种的报告错误的机制，它可以在远离错误发生的地方进行处理错误。所有关于错误发生的的信息必须包含在异常对象中。在错误发生的过程中，你可能想把底层的错误转化成详细的应用程序错误，而且不丢失关于错误的任何信息。你须要仔细考虑关于如何在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 Ap…

Effective C# 原则43：请勿滥用反射
type: item
date: 2015-01-25T20:27:00+08:00
description: 创建二进制的组件时，同时也意味着你要使用迟后绑定和反射来查找你所须要的具有特殊功能代码。反射是一个很有力的工具，而且它让你可以写出可动态配置的软件。使用反射，一个应用程序可以通过添加新的组件来更新功能，而这些组件是在软件最开始发布时没有的。这是有利的。这一伸缩性也带来了一些复杂的问题，而且复杂问题的增加又会增加出现其它问题的可能。当你使用反射时，你是围绕着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()方法上返回的参数列表中，检测所有出现的参数类型。这样的代码是很沉长的，而且很糟糕以至于我…

Effective C# 原则42：使用特性进行简单的反射
type: item
date: 2015-01-24T19:50:00+08:00
description: 当你创建了一个与反射相关的系统时，你应该为你自己的类型，方法，以及属性定义一些自己的特性，这样可以让它们更容易的被访问。自定义的特性标示了你想让这些方法在运行时如何被使用。特性可以测试一些目标对象上的属性。测试这些属性可以最小化因为反射时可能而产生的类型错误。假设你须要创建一个机制，用于在运行时的软件上添加一个菜单条目到一个命令句柄上。这个须要很简单：放一个程序集到目录里，然后程序可以自己发现关于它的一些新菜单条目以及新的菜单命令。这是利用反射可以完成的最好的工作之一：你的主程序须要与一些还没有编写的程序集进行交互。这个新的插件同样不用描述某个集合的功能，因为这可以很好的用接口来完成编码。让我们为创建一个框架的插件来开始动手写代码吧。你须要通过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 ) { …

Effective C# 原则41：选择DataSet而不是自定义的数据结构
type: item
date: 2015-01-23T21:16:00+08:00
description: 因为两个原则，把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可以知道什么列要添加。它会在集合中的第一个元素上发现所有的公共属性，然后显示他们。当集合为空时，这就不能工作了。你有两…

Effective C# 原则40：根据需求选择集合
type: item
date: 2015-01-21T22:29:00+08:00
description: “哪种集合是最好的？”答案是：“视情况而定。” 不同的集合有不同的性能，而且在不同的行为上有不同的优化。.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个元素。在多维数组上的每一维的长度总是一致的。编译器利用这一特性，可以创建更高效的代码。初始化锯齿数组时须要多次使用初始语句。…