.net约定、惯用法与模式
第一章概述
PC早期底层编程工具:编译器、API、标准程序库
OOP产生框架的概念
为确保可重用组件的一致性,需要共同的规则
1.1精心设计的框架所应具备的品质
简单:功能强大和简单之间进行平衡
设计代价高:对用户来说,框架实现细节不可见
充满利弊权衡:
应该借鉴过去:借鉴经过实践检验的设计
要考虑未来发展:对框架将来的发展能力有怎样的影响
良好的集成性:
一致性:
第二章框架设计基础
要设计既功能强大又易于使用的框架:简单的东西是简单的,复杂的东西是可能的;80/20
要明确的为具有不同编程风格、需求、技能以及不同编程语言的人设计框架
要了解那些使用多语言框架的广大开发人员
2.1渐进框架
.net framework:覆盖广大开发人员但不是所有人员
2.2框架设计的基本原则
1场景驱动设计的原则
在设计框架时,必须从一组使用场景以及实现这些场景的样例代码开始:集中精力在常用场景
要确保对任何包含公用API的特性的设计来说,其核心部分都是API设计规范。
要为每个主要的特性域定义一些最常用的使用场景。
要确保使用场景与适当的抽象层次相对应。
要以这样的方式来设计API:先为主要的使用场景来编写样例代码,然后再定义对象模型来支持这些代码。
要用至少两种不同的编程语言来为主要场景编写案例代码。
不要在设计框架的公用API时完全依赖于标准的设计方法。
要安排可用性研究来测试用于主要场景的API。
2低门槛原则
框架必须以易于实验的方式来为普通用户提供一个低门槛
确保每个特性域的名字空间只包含用于常用场景的类型。用于高级场景的类型放入子名字空间。
要为构造函数和方法提供简单的重载函数。
不要在为主要场景设计的类型中包含用于高级场景的方法。
不要要求用户在基本的场景中显示地实例化一个以上的类型。
不要要求用户在为基本使用场景编写代码之前就进行大量地初始化。
要尽可能地为所有的属性和参数提供合适的默认值。
要通过异常来传达对API的误用。
3自说明对象模型原则
在简单的使用场景中,一定要让框架无需文档就能使用。
要确保API是直观的,无需查阅参考文档就能用于简单场景。
要为所有的API提供优秀的文档。
要在规范检查中重视标识符名称的选择。
不要担心标识符的名字太冗长。
要在设计过程的早期让用户教育专家参与。
考虑把最好的名字留给最常用的类型。
要通过异常消息告诉开发人员对框架的误用。
要尽可能的提供强类型API。
要确保与.net框架或客户可能会使用的其他框架保持一致。
避免在主要场景的API中使用过多的抽象。
4分层架构原则
分层设计使得在单个框架中同时提供强大的功能和易用性成为可能。
考虑对框架进行分层,使高层API能提供最佳的开发效率,低层API能提供最强大的功能和最丰富表现力。
避免把高层API和低层API放在同一个命名空间中,如果低层API非常复杂的话。
要确保单个特性域中,不同的层能很好的集成在一起。
第三章命名规范
3.1大小写约定
PascalCasing(除参数名外,命名空间,类型,成员):首字母大写,不使用下划线。两字母长度缩写词是特例
camelCasing参数名:第一单词小写,其余单词首字母大写。
两字母缩写词全部大写,除非是camelCasing的第一个单词。
复合词当成一个单词。
不要以为所有的编程语言都是区分大小写的,不应该仅仅通过大小写来区分名字。
3.2通用命名约定
要为标识符选择易于阅读的名字。
要更看重可读性,而不是更看重简短性。
不要使用下划线、连字符及其他不是字母和数字的字符。
不要使用匈牙利命名法。
避免使用与常用的编程语言的关键字有冲突的标识符。
不要使用缩写词作为标识符名字的一部分。
不要使用未被广泛接收的首字母缩写词。
避免使用语言特有的关键字。
要在创建已有API的新版本时使用与旧API相似的名字。
要优先使用后缀表示API的新版本。
要使用数字后缀表示API的新版本。
可考虑使用全新但有意义的标识符。
不要使用类似Ex的后缀表示新版本。
对64位整数进行操作的新API使用64后缀。
3.3程序集和DLL的命名
要为程序集和DLL选择提示性的名字。不一定和命名空间对应。
考虑命名模式:Company.Component.dll
3.4命名空间的命名
Company.(Product|Technology)[.Feature][.Subnamespace]
用公司名称作命名空间前缀。
要用稳定的、与版本无关的产品名称作空间的第二层。
不要根据公司的组织架构决定命名空间层次。
使用Pascal大小写,用点分割。
考虑适当的时候用复数形式。
不要用相同的名字命名空间和类型。
不要引入太一般化的类型名。
1应用程序模型命名空间System.Web.UI,System.Windows
不要给位于同一个应用程序模型的命名空间中的类型起相同的名字。
2基础设施命名空间System.Windows.Forms.Design
3核心名字空间 除1,2外的System
不要给类型起会与核心命名空间中的任何类型冲突的名字。
4技术命名空间组
不要给类型起会与技术空间中其他类型冲突名字。
不要在技术空间与应用模型空间的类型间引入名字冲突。
3.5类、结构和接口的命名
如果无法为类型提供名词词组,那么应重新考虑总体设计。
要用名词词组给类型命名,少数情况可用形容词词组。
不要给类名加前缀。
考虑让派生类的名字以基类结尾。
要让接口的名字以I开头。
要确保接口的标准实现和接口只相差一个I前缀。
1泛型类型参数的命名
要用描述性的名字来命名泛型类型参数。
考虑用T来命名参数类型。
要在描述性类型参数加T前缀。
考虑参数名中显示限制。
2常用类型的命名
派生或实现核心类型规则:
Attribute,EventArgs,Exception,Dictionary,Stream后缀
Delegate派生类加事件处理加EventHandler,事件处理之外加Callback,不要Delegate后缀
IEnumerable,ICollection,IList及对应泛型加Colletion后缀
IPermission,AccessPermission加Permission后缀
不要派生自ENUM,用关键字代替
3枚举类型的命名
要用单数名词表示enum类型,除非表示位域。
要用复数名词来表示位域的枚举类型。
不要给枚举类型添加Enum后缀。
不要给枚举类型添加Flag后缀。
不要给枚举类型值的名字添加前缀。
3.6类型成员的命名
1方法命名
要用动词或动词词组来命名方法。
2属性的命名
要用名词或形容词词组来命名属性。
不用让属性名看起来与get方法名相似。
要用肯定性的短语来命名布尔属性。
考虑用属性的类型名来命名属性。
3事件的命名
要用动词或动词短语来命名事件。
要用现在时和过去时来赋予事件名以之前和之后的状态。
不要用Before或After前缀或后缀。
要在命名事件处理函数时加上EventHandler后缀。
要在事件处理函数中用sender和e作为两个参数的名字。
要在命名事件的参数时加上EventArgs后缀。
4字段的命名
适用于静态公有字段和静态受保护字段
要用名词或名词短语来命名字段。
不要添加字段前缀。
3.7参数的命名
命名参数时使用camelCasing风格。
要使用具有描述性的参数名。
考虑根据参数的意思而不是类型来命名参数。
3.8资源的命名
和属性命名相似。
要使标识符的名字具有描述性而不是变短。
不要使用各CLR语言特有的关键字。
要在命名资源时仅使用字母数字下划线。
要用点号来为标识符清楚的划分层次。
要在为异常的消息资源命名时遵循异常的类型名加上点然后加上简短的标识符
第四章类型设计规范
要确保每个类型由一组定义明确、相互关联的成员组成,而不是一些无关功能的随机集合。
4.1类型和名字空间
要用名字空间把类型组织成一个相关的特性域的组织结构。
避免非常深的名字空间层次。
避免有太多的名字空间。
避免把为高级场景设计的类型和为常见编程而设计的类型放在同一个命名空间中。
不要不指定命名空间就定义类型。
1Design子名字空间
要用.Design后缀的名字空间来容纳那些为基本名字空间提供设计时功能的类型。
2Permissions子名字空间
要用. Permissions后缀的名字空间来容纳那些为基本名字空间提供自定义许可的类型。
3Interop子名字空间
要用. Interop后缀的名字空间来容纳那些为基本名字空间提供互操作功能的类型。
要用. Interop后缀的名字空间来容纳所有PLA中的代码。
4.2类和结构之间的选择
不要定义结构,除非有如下特征:逻辑上代表独立的值、实例小于16字节、不可变;实例比较小周期比较短,经常内嵌。
4.3类和接口之间的选择
要优先采用类而不是接口。
要用抽象类而不是接口来解除协定与实现之间的耦合。
要定义接口,如果需要提供一个多态的值类型层次结构的话。
考虑通过定义接口来达到与多重继承相类似的效果。
4.4抽象类的设计
不要在抽象类型中定义公有的或内部受保护的构造函数。
要为抽象类定义受保护的构造函数或内部构造函数。
要为你发布的抽象类提供至少一个继承自该类的具体类型。
4.5静态类的设计
要尽量少用静态类。
不要把静态类当作是杂物箱。
不要在静态类中声明或覆盖实例成员。
要把静态类定义成密封的抽象的,并添加一个私有的实例构造函数,如果你的编程语言没有提供对静态类的支持。
4.6接口的设计
要定义接口,如果你需要包括值类型在内的一组类型支持一些公共的API。
考虑定义接口,如果你需要让已经自其他类型继承的类型支持该接口提供的功能。
避免使用记号接口(没有成员)。
要为接口提供至少一个实现该接口的类型。
要为你定义的每个接口提供至少一个使用该接口的API。
不要给已经发行的接口再添加成员。
4.7结构的设计
不要为结构提供默认的构造函数。
要确保当所有的实例数据都为0,false,null时,结构仍处于有效状态。
要为值类型实现IEquatable<T>。
不要显示的扩展System.ValueType,事实上多数编程语言不允许这样做。
4.8枚举的设计
要用枚举来加强那些表示值的集合的参数、属性以及返回值的类型性。
要优先使用枚举而不是使用静态常量。
不要把枚举用于开放的集合。
不要提供为了今后使用而保留的枚举值。
避免显示的暴露只有一个值的枚举。
不要把sentinel值包含在枚举值中。
要为简单枚举类型提供零值。
考虑用Int32作为基本的枚举实现类型,除非某一条成立:标记枚举超过32个标记;为了更方便的与需要不同大小的枚举的非托管代码进行互操作,基本的实现类型必须是Int32之外的类型;更小的底层实现类型可能会节省相当的空间。
要用复数名词或名词短语来命名标记枚举,单数名词或短语来命名简单枚举。
不要直接扩充System.Enum
4.8.1标记枚举的设计
要对标记枚举使用System.FlagsAttribute。
要用2的幂次方作为标记枚举的值。
考虑为常用的标记集合提供特殊的枚举值。
避免让创建的标记枚举包含无效的组合。
避免把0用作标记枚举的值,除非该值表示所有标记都被清除并且正确命名。
要把标记枚举的0值命名为None。
4.8.2给枚举添加值
考虑给枚举添加值,尽管可能有一点兼容性的风险。
4.9嵌套类型
要在想让一个类型能够访问外层类型的成员时才使用嵌套类型。
不要用嵌套类型来进行逻辑分组,而应该用命名空间。
避免公开的暴露嵌套类型。
不要使用嵌套类型,如果该类型可能会被除了他的外层类型之外的类型引用。
不要使用嵌套类型,如果他们需要被客户代码实例化。
不要把嵌套类型定义为接口的成员。
第五章成员设计
5.1成员设计的一般规范
5.1.1成员重载
要尽量用描述性的参数名来说明在较短的重载中所使用的默认值。
避免在重载中随意的给参数命名。
避免使重载成员的参数顺序不一致。
要把最长的重载做成虚函数(如果需要可扩展性)。
不要在重载成员中使用ref或out修饰符。
要允许可选参数为null。
要优先使用成员重载,而不是定义有默认参数的成员。
5.1.2显式的实现接口成员
避免显式的实现接口成员,如果没有很强的理由。
考虑显式的实现接口成员,如果希望接口成员只能通过该接口来调用。
考虑通过显式的实现接口成员来模拟variance(在被覆盖的成员中改变参数或返回值的类型)。
考虑在需要隐藏一个成员并增加一个名字更加合适的等价成员时,显式的实现接口成员。
不要把接口成员的显式实现用作安全壁垒。
要为显式实现的接口成员提供一个功能相同的受保护的虚成员,如果希望让派生类对该功能进行定制。
5.1.3属性和方法之间的选择
考虑使用属性,如果该成员表示类型的逻辑attribute。
要使用属性而不要使用方法——如果属性的值存储在内存中,而且提供属性的目的仅是为了访问该值。
要在下列情况使用方法而不是属性:该操作比字段访问要慢一个或多个数量级;该操作是一个转换操作;该操作每次返回的结果不同,即使传入的参数不变;该操作有严重的能观察到的副作用;该操作返回内部状态的一个副本;该操作返回一个数组;
5.2属性的设计
要创建只读属性,如果不应该让调用方改变属性的值。
不要提供只写属性,也不要让设置方法的存取范围比获取方法更广。
要为所有的属性提供合理的默认值。
要允许用户以任何顺序来设属性的值,尽管这可能会使对象在短时间内处于无效状态。
要保留属性原来的值,如果属性的设置方法抛出异常。
避免在属性的获取方法中抛出异常。
5.2.1索引属性的设计
考虑通过索引器的方式让用户访问存储在内部数组中的数据。
考虑为代表项集的类型提供索引器。
避免有一个以上参数的索引器。
避免用Int32,Int64,String,Object,枚举,泛型以外的类型来作索引器的参数。
要用Item来作索引属性的名字,除非有更合适的名字。
不要同时提供语义上等价的索引器和方法。
不要在一个类型中提供具有不通名字的索引器。
不要使用非默认的索引属性。
5.2.2属性改变的通知事件
考虑在高层API的属性值被修改时触发属性改变的通知事件。
考虑在属性值被外界修改时触发通知事件。
5.3构造函数的设计
考虑提供简单的构造函数,最好是默认构造函数。
考虑用静态的工厂方法来代替构造函数——如果无法让想要执行的操作的语义与新实例的构造函数直接对应,或遵循构造函数的设计规范会感觉不自然。
要把构造函数的参数用作设置主要属性的便捷方法。
要使用相同的名字命名构造函数参数和属性,如果参数用于简单设置属性。
要在构造函数中做最少的工作。
要从实例构造函数中抛出异常,如果合适。
要在类中显式的声明公用默认构造函数,如果这样的构造函数是必须的。
避免在结构中显式的定义默认构造函数。
避免在对象的构造函数内部调用虚成员。
要把静态构造函数声明为私有。
不要在静态构造函数中抛出异常。
考虑以内联得形式来初始化静态字段,而不要显式的定义静态构造函数,这是因为运行库能够对那些没有显式定义静态构造函数的类型进行性能优化。
5.4事件的设计
要在事件中使用术语raise,而不要使用fire或trigger。
要用EventHandler<T>来定义事件处理函数,不要手工创建新的委托。
考虑用EventArgs的子类来做事件的参数,除非百分之百相信不需要给事件处理方法传递数据,可以直接使用EventArgs。
要用受保护的虚函数来触发事件。这只适用于非密封类中的非静态事件,不适用于结构、密封类及静态事件。方法的名字以On开头。
要让触发事件的受保护方法带一个参数,该参数的类型为事件参数类,名字为e。
不要在触发非静态事件时把null作为sender参数传入。
不要在触发事件时把null作为数据参数传入。
考虑触发能够被最终用户取消的事件,这只适用于前置事件。
5.4.1自定义事件处理函数的设计
要把事件处理函数的返回类型定义为void。
要用object作为事件处理函数的第一个参数的类型,并命名为sender。
要用EventArgs或其子类作为事件处理函数的第二个参数的类型,并命名为e。
不要在事件处理函数中定义两个以上的参数。
5.5字段的设计
不要提供公有的或受保护的实例字段。
要用常量字段来表示永远不会改变的常量。
要用公有的静态只读字段来定义预定义的对象实例。
不要把可变类型的实例赋给只读字段。
5.6操作符重载
避免定义操作符重载,除非该类型看起来应该像个基本类型。
考虑在看起来应该象基本类型的类型中定义操作符重载。
要为表示数值的结构定义操作符重载。
不要在定义操作符重载时耍小聪明。
不要提供操作符重载,除非至少有一个操作数的类型就是定义该重载的类型。
要以对称的方式来重载操作符。
考虑为每个重载过的操作符提供对应的方法,并用容易理解的名字命名。
5.6.1重载operator==
5.6.2类型转换操作符
不要提供类型转换操作符,如果没有明确的用户需求。
不要定义位于类型的领域之外的类型转换操作符。
不要提供隐式类型转换操作符,如果该类型转换可能会丢失精度。
不要在隐式的类型转换中抛出异常。
要抛出InvalidCastException,如果类型转换会丢失精度而该操作符承诺不丢失精度。
5.7参数的设计
要用类层次结构中最接近基类的类型来作为参数的类型,同时要保证该类型能提供成员所需的功能。
不要使用保留参数。
不要把指针、指针数组、多维数组作为公有方法的参数。
要把所有的输出参数放在所有以值和引用方式传递的参数(不包括参数数组)后面,即使会导致重载成员参数顺序不一致。
要在重载成员或实现接口成员时保持参数命名一致。
5.7.1枚举和布尔参数之间的选择
要用枚举,如果不这样做会导致参数中有两个以上的布尔类型。
不要使用布尔参数,除非百分之百确定绝对不需要两个以上的值。
考虑在构造函数中,对确实只要两种状态值的参数以及用来初始化布尔类型属性的参数使用布尔类型。
5.7.2参数的验证
要验证传给公有的、受保护的或显式实现的成员的参数。如果验证失败,抛出ArgumentException或子类。
要抛出ArgumentNullException,如果传入null而该成员不支持null。
要验证枚举参数。
不用用Enum.IsDefined来检查枚举的范围。
要清楚的知道传入的可变参数可能会在验证后发生改变。
5.7.3参数的传递
避免使用输出参数或引用参数。
不要以引用方式传递引用类型。
5.7.4参数数量可变的成员
考虑给数组参数增加params关键字,如果预计用户会传入为数不多的数组元素。
避免使用params数组参数,如果调用方几乎总是有现成的数组作为输入。
不要使用params数组参数,如果数组会被以其为参数的成员修改。
考虑在简单的重载中使用params关键字,尽管更复杂的重载不能用params。
要对参数进行合理的排序,以便使用params关键字。
考虑在对性能要求非常高的API中为参数数量较少的调用提供特殊的重载和相应的实现。
要注意传入的params数组参数可能是null。
不要使用varargs方法又称省略号。
5.7.5指针参数
要为任何以指针为参数的成员提供一个替补成员,因为指针不符合CLS规范。
避免对指针参数进行高开销的检查。
要在设计用到指针的成员时遵循与指针相关的常用约定。
第6章为扩展性而设计
6.1扩展机制
6.1.1非密封类
考虑用不包括任何虚成员和保护成员的非密封类来为框架提供扩展性,这种方法开销不高。
6.1.2保护成员
考虑将保护成员用于高级定制。
要在对安全性、文档及兼容性进行分析时,把非密封类中保护成员当成公有成员对待。
6.1.3事件与回调函数
考虑使用回调函数来让框架能够执行用户提供的代码。
考虑使用事件来让用户定制框架的行为,这样不需用户对面向对象有深入理解。
要优先使用事件,而不是简单的回调函数,原因是开发人员更熟悉事件。
避免在对性能要求很高的API中使用回调函数。
要理解在调用委托时可以执行任何代码,这可能会引起安全性、正确性、兼容性问题。
6.1.4虚成员
不要使用虚成员,除非有合适的理由,同时你对设计、测试、维护虚成员的开销有清楚的认识。
考虑只有在绝对必须的时候才用虚成员提供扩展性,并采用Template Method模式。
要优先使用受保护的虚成员,而不是公有虚成员。
6.1.5抽象(抽象类型和抽象接口)
不要提供抽象,除非为该抽象开发出具体实现并用API对其进行过实际测试。
要在设计抽象时谨慎的选择抽象类或是抽象接口。
考虑为抽象的具体实现提供参考测试。
6.2基类
考虑使基类为抽象类,即使他们不包含任何抽象成员。
考虑把基类与用于主要场景的类型分开,放到单独的名字空间中。
避免在命名基类时使用Base后缀,如果该类会用于公用API。
6.3密封
不要把类密封起来,除非有恰当的理由。
不要在密封类中声明保护成员或虚函数。
考虑在覆盖成员时将其密封。
第7章异常
7.1抛出异常
不要返回错误码。
要通过抛出异常的方式来报告操作失败。
考虑通过调用Environment.FailFase来结束进程,而不要抛出异常,如果代码遇到严重问题,已经无法安全的执行。
不要在正常的控制流中使用异常,如果可以避免的话。
考虑抛出异常可能会对性能造成的影响。
要为所有的异常撰写文档,并把他们作为协定的一部分,前提是这些异常是由于违反了公有成员的协定而抛出的。
不要让公有成员根据某个选项来决定是否抛出异常。
不要把异常用作公有成员的返回值或输出参数。
考虑使用辅助方法来创建异常。
不要在异常筛选器中抛出异常。
避免显式的从finally块中抛出异常。
7.2为抛出的异常选择合适的类型
考虑优先使用已有的异常,而不是创建新的异常类型。
要使用自定义的异常类型,如果对错误的处理方式与其他已存在的异常类型不同。
不要仅仅为了拥有自己的异常而创建并使用新的异常。
要使用最合理、最具针对性的异常。
7.2.1错误消息的设计
要在抛出异常时为开发人员提供丰富而有意义的错误消息。
要确保异常消息的语法正确无误。
要确保异常消息中的每个句子都有句号。
避免在异常消息中使用问号和惊叹号。
不要在没有得到许可的情况下在异常消息中泄漏安全信息。
考虑把组件抛出的异常消息本地化,如果希望组件为不同国家开发人员使用。
7.2.2异常处理
不要在框架的代码中,在捕获具体类型不确定的异常时,把错误吞了。
避免在应用程序的代码中,在捕获具体类型不确定的异常时,把错误吞了。
不要在为了转移异常而编写的catch代码块中把任何特殊的异常排除在外。
考虑捕获特定类型的异常,如果理解该异常产生的原因并可以作出适当的反应。
不要捕获不应该捕获的异常。
要在进行清理工作时使用try-finally,避免使用try-catch。
要在捕获并重新抛出异常时使用空的throw语句。
不要用无参数的catch块来处理不符合CLS规范的异常。
7.2.3对异常进行封装
考虑对较低层次抛出的异常进行适当的封装,如果较低层次的异常在较高层次的运行环境中没有什么实际的意义。
避免捕获并封装具体类型不确定的异常。
要在对异常进行封装时为其指定内部异常。
7.3标准异常类型的使用
7.3.1Exception与SystemException
不要抛出Exception和SystemException异常。
不要打算在框架代码中捕获这两种异常,除非打算重新抛出。
避免捕获这两种异常,除非是在顶层异常处理器中。
7.3.2ApplicationException
不要抛出ApplicationException或从他派生新类。
7.3.3InvalidOperationException
要抛出InvalidOperationException异常,如果对象处于不正确的状态。
7.3.4ArgumentException,ArgumentNullException,ArgumentOutOfRangeException
要抛出ArgumentException或其子类,如果传入的是无效参数。
要在抛出ArgumentException或其子类异常时设置ParamName属性。
要在属性的设置方法中,以value作为隐式值参数的名字。
7.3.5NullReference,IndexOutOfRange,AccessViolation
不要让公用API显式的或隐式的抛出这几个异常。
7.3.6StackOverflow
不要显式的抛出StackOverflow异常。
不要捕获StackOverflow异常。
7.3.7OutOfMemory
不要显式的抛出OutOfMemory异常。
7.3.8ComException,SEHException及其他CLR异常
不要显式的抛出Interop,Com,SEH异常。
不要显式的捕获SHE异常。
7.3.9ExecutionEngine
不要显式的抛出ExecutionEngine异常。
7.4自定义异常的设计
避免太深的继承层次。
要从Exception或其他常用基类派生新的异常类。
要在命名异常类时使用Exception后缀。
要使异常可序列化。
要为所有的异常提供常用的构造函数。
要通过ToString的重载方法报告对安全敏感的信息,前提是必须获得相应的许可。
要把与安全性有关的信息保存在私有的异常状态中。
考虑为异常定义属性,这样就能从程序中取得异常的额外信息。
7.5异常与性能
不要因异常可能对性能造成的负面影响而使用错误码。
7.5.1Tester-Doer模式
考虑在方法中使用Tester-Doer模式来避免因异常而引起的性能问题,如果该方法在普通的场景中都可能会抛出异常。(可能会引起竞态条件,使用须小心)
7.5.2Try-Parse模式
考虑在方法中使用Try-Parse模式来避免因异常而引起的性能问题,如果该方法在普通的场景中都可能会抛出异常。
要在实现Try-Parse模式时使用Try前缀,并用布尔作为该方法的返回类型。
要为每个使用Try-Parse模式的方法提供一个会抛出异常的对应成员。
第8章使用规范
8.1数组
要在公用API中优先使用集合,而不是数组。
不要使用只读的数组字段。
考虑使用不规则数组,而不要使用多维数组。
8.2Attribute
要在命名自定义attribute类时添加Attribute后缀。
要在定义自己的attribute时使用AttributeUsageAttribute。
要为可选参数提供可设置的属性。
要为必填参数提供只读属性。
要提供构造函数参数来对必填参数进行初始化。每个参数名和属性名相同。
避免提供构造函数参数来对可选参数进行初始化。
避免对自定义attribute的构造函数进行重载。
要尽可能将自定义attribute类封装起来。
8.3集合
不要在公用API中使用弱类型集合。
不要在公用API中使用ArrayList或List<T>。
不要在公用API中使用Hashtable或Dictionary<TKey,TValue>。应使用IDictionary。
不要使用IEnumerator<T>,IEnumerator或实现两个接口之一的任何类型,除非是作为GetEnumerator方法的返回值。
不要在一个类型中同时实现IEnumerator<T>,IEnumerable<T>。对非泛型接口同样适用。
8.3.1集合参数
要用最泛的类型作为参数类型。大多数使用IEnumerable<T>接口。
避免使用ICollection<T>或ICollection来做参数,如果其目的仅是为了访问接口的Count属性。
8.3.2集合属性与返回值
不要提供可设置的集合属性。
要用Collection<T>或其子类作为属性或返回值来表示可读写的集合。
要用ReadOnlyCollection<T>或其子类作为属性或返回值来表示只读的集合。
考虑使用泛型集合基类的子类,而不要直接使用该集合。
要用Collection<T>,ReadOnlyCollection<T>的子类作为常用方法和属性的返回值。
考虑使用有键集合,如果集合中存储的元素都有独一无二的键值。
不要从集合属性或以集合为返回值的方法中返回null。可返回一个空集合。
1SnapshotCollection与LiveCollection
不要让属性返回SnapshotCollection,属性应返回LiveCollection。
要用SnapshotCollection或Live IEnumerable<T>表示不稳定的集合。
8.3.3数组和集合之间的选择
要优先使用集合,而不是数组。
考虑在低层API中使用数组,以降低内存的消耗提高性能。
要使用字节数组,而不要使用字节集合。
不要将数组用于属性,如果每次调用属性的获取方法时都必须返回一个新数组。
8.3.4自定义集合的实现
不要继承自非泛型的集合基类。
要为强类型的非泛型集合实现IEnumerable<T>。
避免为类型实现集合接口,如果类型的API很复杂,而且与集合的概念无关。
要在为实现IEnumerable<T>接口的类型命名时添加Collection后缀,除非同时实现IDictionary或IDictionary<TKey,TValue>。
要在为实现IDictionary或IDictionary<TKey,TValue>的类型命名时添加Dictionary后缀。
避免给集合抽象的名字添加代表具体实现的后缀。
考虑用集合元素的类型名作为集合名字的前缀。
考虑给只读集合的名字添加ReadOnly前缀。
8.4ICloneable
不要实现ICloneable。
不要在公用API中使用ICloneable。
考虑为需要运用克隆机制的类型定义Clone方法。
8.5IComparable<T>与IEquatable<T>
要为值类型实现IEquatable<T>。
要在实现IEquatable<T>.Equals方法时,遵循覆盖Object.Equals方法的规范。
要在实现IEquatable<T>.Equals方法的同时覆盖Object.Equals。
考虑在实现IEquatable<T>的同时重载operator==和!=。
要在实现IComparable<T>的同时实现IEquatable<T>。
考虑在实现IComparable<T>的同时重载比较操作符<><= >=。
8.6IDisposable
8.7对象
8.7.1Object.Equals
要在覆盖Object.Equals时,遵循他定义的协定。
要在覆盖Equals方法的同时,覆盖GetHashCode方法。
考虑在覆盖Object.Equals的同时实现IEquatable<T>接口。
不要从Equals方法中抛出异常。
1值类型的Equals
要覆盖值类型的Equals方法。
要通过实现IEquatable<T>来提供一个以值类型本身为参数的Equals重载方法。
2引用类型的Equals
考虑对Equals进行覆盖以提供值相等语义,如果引用类型表示的是一个值。
8.7.2Object.GetHashCode
要覆盖GetHashCode方法,如果覆盖了Object.Equals方法。
要确保对任何两个对象来说,如果Object.Equals方法返回true,GetHashCode返回值也相同。
要尽量让类型的GetHashCode方法产生随机分布的散列码。
要确保无论怎么更改对象,GetHashCode都会返回万全相同的值。
避免从GetHashCode中抛出异常。
8.7.3Object.ToString
要覆盖ToString方法,只有返回既有用又易于阅读的文字。
要尽量让ToString方法返回短小的字符串。
考虑为每一个实例返回一个独一无二的字符串。
要使用易于阅读的名字,不要为了独一无二使用难以阅读的ID。
要在返回与culture有关的信息时,根据当前线程的culture对字符串格式化。
要提供重载方法ToString(string format)或实现IFormattable接口。
不要从ToString方法返回空字符串或null。
避免从ToString方法抛出异常。
要确保ToString方法不会产生副作用。
要通过覆盖ToString报告对安全敏感的信息,前提是获得许可。
考虑让ToString方法输出的字符串能够为该类型的解析方法正确的解析。
8.8Uri
要使用Uri表示URI和URL的数据。
考虑为最常用的带Uri参数的成员提供基于字符串的重载成员。
不要为所有基于Uri的成员提供基于字符产的重载成员。
要调用基于Uri的重载成员,如果有的话。
不要在字符串中存储URI/URL数据。
8.9Xml的使用
不要用XmlNode或XmlDocument来表示xml数据。要用IXPathNavigable来代替。
要用XmlReader作为方法的xml数据输入,或用IXPathNavigable作为方法返回的xml。
要为类型实现IXPathNavigable接口,如果该类型表示下层对象模型或数据源的xml视图。
不要从XmlDocumeng派生子类,如果该类型表示下层对象模型或数据源的xml视图。
8.10相等性操作符
不要只重载相等性操作符中的一个。
要确保Object.Equals与相等性操作符具有万全相同的语义和相近的性能。
避免从相等性操作符中抛出异常。
8.10.1值类型的相等性操作符
要重载值类型的相等性操作符,如果相等性是有意义的。
8.10.2引用类型的相等性操作符
避免重载可变引用类型的相等性操作符。
考虑不要重载引用类型的相等性操作符,即使覆盖了Object.Equals或实现了IEquatable<T>。
避免重载引用类型的相等性操作符,如果其实现回比引用相等性慢的多。
第9章常用的设计模式
9.1聚合组件
把多个低层类型集中到一个高层类型中,以次来支持常用场景。
9.1.1面向组件的设计
聚合组件是一个façade,他基于面向组件的设计,并满足:
构造函数:聚合组件应该有默认构造函数。
构造函数:构造函数的所有参数应该与属性对应,并用来初始化属性。
属性:大多数属性应该有获取方法和设置方法。
属性:所有属性都有合理的默认值。
方法:如果参数在方法调用之间不会改变,那么方法就不应该带这样的参数。这样的选项应该通过属性指定。
事件:方法不以委托为参数。所有回调函数都通过事件实现。
9.1.2因子类型
9.1.3聚合组件规范
考虑为常用的特性域提供聚合组件。
要用聚合组件来为高层的概念(物理实体),而不是系统级的任务建模。
要让聚合组件的名字与众所周知的系统实体相对应,使类型更引人注目。
要在设计聚合组件时使初始化尽可能简单,只需简单初始化就可使用。
不要要求聚合组件的用户在简单场景中显式的实例化多个对象。
要保证让聚合组件支持Create-Set-Call使用方式,以此实现大多数场景。
要为所有聚合组件提供默认构造函数或非常简单的构造函数。
要为聚合组件提供带获取方法和设置方法的属性,来对应构造函数中的所有参数。
要在聚合组件中使用事件,而不用使用基于委托的API。
考虑用事件来代替需要被覆盖的虚成员。
不要要求聚合组件的用户在常用场景中使用继承、覆盖方法或实现接口。
不要要求聚合组件的用户在常用场景中除了编码之外,还要做其他工作。
考虑让聚合组件能够自动切换模式。
不要设计有多种模式的因子类型。
考虑将聚合组件集成到vs设计器中。
考虑把聚合组件和因子类型分开,各自放到不同的程序集中。
考虑把聚合组件的内部因子类型暴露给外界访问。
9.2Async模式
要在为异步操作定义API时遵循如下约定:应该提供Begin和End加同步方法名的方法,并设置合适的参数和返回类型。
要确保Begin方法的返回类型实现了IAsyncResult接口。
要确保同步方法的按值传递和按引用传递的参数在Begin方法中都是按值传递的,同步方法的输出参数不应该出现在Begin方法中。
要确保End方法的返回类型与同步方法相同。
要确保同步方法的输出参数和按引用传递的参数都作为End方法的输出参数,同步方法的按值传递的参数不出现在End方法中。
不要继续执行异步操作,如果Begin方法抛出了异常。
要依次通过如下机制告诉调用方异步操作已经完成:将IAsyncResult.IsCompleted设为true;调用Async回调函数;激活IAsyncResult.AsyncWaitHandler返回的等待句柄。
要通过从End方法中抛出异常来表示无法成功的完成异步操作。
要在End方法被调用时完成所有没有完成的操作。
考虑抛出InvalidOperationException,如果用户用同一个IAsyncResut两次调用End方法,或IAsyncResult是从另一个不相关的Begin方法返回的。
要把IAsyncResult.CompletedSynchronously设为true,当且仅当Async回调函数将在调用Begin方法的线程中运行时。
9.3Dispose模式
要为含有可处置类型实例的类型实现基本Dispose模式。
要为类型实现基本Dispose模式并提供终结方法,如果类型持有需要开发人员显式释放的类型,而且后者本身没有终结方法。
考虑为类实现基本Dispose模式,如果类本身并不持有非托管资源或可处理对象,但他的子类却可能会持有。
9.3.1基本Dispose模式
要声明protected virtual void Dispose(bool disposing)方法来把所有与非托管资源清理相关的工作放在一起。
要按如下步骤实现IDisposable接口,先调用Dispose(true),再调用GC.SuppressFinalize(this)。
不要使无参数的Dispose方法为虚方法。
不要为Dispose声明除Dispose()和Dispose(bool)之外的其他重载方法。
要允许多次调用Dispose(bool)方法。他可以在第一次调用后就什么也不做。
避免从Dispose(bool)中抛出异常,除非紧急情况,所处的线程被破坏。
要从成员中抛出ObjectDisposedException异常,如果成员在对象被dispose后就无法再使用。
考虑在Dispose方法之外再提供一个Close方法,如果Close是该领域标准术语。
9.3.2可终结类型
避免使类型为可终结。
不要使值类型为可终结。
要使类型为可终结,如果类型要负责释放本身并不具备终结方法的非托管资源。
要为所有的可终结类型实现基本Dispose模式。
不要在终结方法调用的代码中访问任何可终结对象。
要使Finalize方法为受保护的。
不要在终结方法中放过任何异常,除非是致命的系统错误。
考虑创建一个用于紧急情况的可终结对象,如果应用程序域被强制卸出或线程异常退出时都必须执行终结方法。
9.4Factory模式
要优先使用构造函数,而不是优先使用工厂,构造函数一般来说更易于使用,更一致,更方便。
考虑使用工厂,如果构造函数提供的对象创建机制不能实现要求。
要在开发人员可能不清楚待创建对象的类型时使用工厂,比如对基类或接口编程。
考虑使用工厂方法,如果这是唯一的办法使其对用户来说不解自明。
要在转换风格的操作中使用factory。
要尽量将工厂操作实现为方法,而不是属性。
要通过方法的返回值而不是输出参数来返回新创建的对象实例。
考虑把create和要创建的对象类型名连在一起,以此来命名工厂方法。
考虑把要创建的类型名和factory连在一起,来命名工厂类型。
9.5Optional Feature模式
考虑将Optional Feature模式用于抽象中的可选特性。
要提供一个简单的布尔属性,用户能够用他来检查对象是否支持可选特性。
要在基类中用虚方法来定义可选特性,并抛出NotSupported异常。
9.6Template Method模式
最常见的形式有一或多个非虚成员组成,这些成员通过调用一或多个受保护的虚成员实现。
避免使公有成员为虚成员。
考虑使用Template Method模式来更好的控制扩展性。
考虑将受保护的虚成员命名为非虚成员加Core后缀,如果虚成员为非虚成员提供了扩展点。
9.7超时
要优先让用户通过参数来提供超时长度。
要优先使用TimeSpan来表示超时长度。
要在超时后抛出TimeoutException异常。
不要通过返回错误码的方式来告诉用户发生了超时。
A C#编程风格约定
1通用风格约定
1.1花括号的使用
要把左花括号放在前一条语句的末尾。
要使右括号与左括号所在行的行首对齐,除非括号内只有一条语句。
要把右括号放在新的一行的开始处。
考虑把只有一条语句的代码块和括号放在一行中。
考虑只有一个访问方法的属性的所有花括号放在同一行中。
要使右花括号单独占一行,除非后面是else,else if或while。
避免省略花括号。
1.2空格的使用
要在左花括号后和右花括号前加一个空格。
避免在左花括号前加空格。
要在形式参数的逗号后加空格。
避免在实际参数之间加空格。
避免在左圆括号之后和右圆括号之前加空格。
不要在成员名字和左圆括号之间加空格。
不要在左方括号之后和右方括号前加空格。
不要在控制流语句之前加空格。
避免在二元操作符之前和之后加空格。
不要在一元操作符之前或之后加空格。
1.3缩进的使用
要用4个连续空格进行缩进。
不要用制表符进行缩进。
要对代码块中的内容进行缩进。
要对case代码块进行缩进,尽管没有花括号。
2命名约定
要在命名标识符时遵循框架设计准则,除非是内部字段和私有字段。
要在命名名字空间、类、成员时使用PascalCasing风格,除非内部字段和私有字段。
要用camelCasing风格来命名内部字段和私有字段。
要用camelCasing风格来命名局部变量。
要用camelCasing风格来命名方法的形式参数。
不要使用匈牙利命名法。
避免给局部变量加前缀。
要使用C#语言中对应的别名。
3注释
不要用注释来描述对任何人都显而易见的事。
避免使用块注释语法。
不要把注释放在行尾,除非注释非常短。
4文件的组织
不要在一个源文件中包含一个以上的公用类型,除非有嵌套类,或各类型之间的不同仅在于泛型参数的数量。
要用相同的名字命名源文件及其包含的公有类型。
要用相同的层次来组织文件目录和命名空间。
考虑根据如下顺序和组别对成员分组:所有字段,所有构造函数,公用属性及受保护的属性,方法,事件,所有显式实现的接口成员,内部成员,私有成员,所有嵌套类型。
要把不能公开访问的成员和显式实现的接口成员分别放在自己的#region块中。
考虑在每个组别内根据字母顺序来组织成员。
考虑根据由简单到复杂的顺序来组织重载成员。
要把using指令放到命名空间的声明之外。
转载地址:http://blog.csdn.net/dreamboy0908/article/details/7230549