Microsoft类库开发的设计准则
来源:互联网 发布:非洲有没有帅哥知乎 编辑:程序博客网 时间:2024/04/29 21:44
参考资料http://msdn.microsoft.com/zh-cn/library/ms229042(VS.80).aspx
部分版权所有 2005 Microsoft Corporation。保留所有权利。
部分版权所有 Addison-Wesley Corporation。保留所有权利。
第一部分 命名准则
1.1大小写约定
下列术语描述了标识符的不同大小写形式。
Pascal 大小写
将标识符的首字母和后面连接的每个单词的首字母都大写。可以对三字符或更多字符的标识符使用 Pascal 大小写。例如:
BackColor
大小写混合
标识符的首字母小写,而每个后面连接的单词的首字母都大写。例如:
backColor
大写
标识符中的所有字母都大写。例如:
IO
标识符的大小写规则:
如果标识符由多个单词组成,请不要在各单词之间使用分隔符,如下划线(“_”)或连字符(“-”)等。而应使用大小写来指示每个单词的开头。
下列准则是用于标识符的通用规则。
对于由多个单词组成的所有公共成员、类型及命名空间名称,要使用 Pascal 大小写。
注意,这条规则不适用于实例字段。由于成员设计准则中详细说明的原因,不应使用公共实例字段。
对参数名称使用大小写混合。
下表汇总了标识符的大小写规则,并提供了不同类型标识符的示例。
标识符 | 大小写方式 | 示例 |
类 | Pascal | AppDomain |
枚举类型 | Pascal | ErrorLevel |
枚举值 | Pascal | FatalError |
事件 | Pascal | ValueChanged |
异常类 | Pascal | WebException |
只读的静态字段 | Pascal | RedValue |
接口 | Pascal | IDisposable |
方法 | Pascal | ToString |
命名空间 | Pascal | System.Drawing |
参数 | Camel | typeName |
属性 | Pascal | BackColor |
1.2 通用命名约定
请选择易读的标识符名称。例如,英文属性名称 HorizontalAlignment 比 AlignmentHorizontal 更具可读性。
可读性比简洁性更重要。属性名称 CanScrollHorizontally 比 ScrollableX(指 X 轴,但意义不明确)更好。
不要使用下划线、连字符或任何其他非字母数字字符。
不要使用匈牙利表示法。
匈牙利表示法是在标识符中使用一个前缀对参数的某些元数据进行编码,如标识符的数据类型。
1.3 程序集和 DLL 的名称
大多数情况下,程序集包含全部或部分可重用库,且它包含在单个动态链接库 (DLL) 中。一个程序集可拆分到多个 DLL 中,但这非常少见,在此准则中也没有说明。
程序集和 DLL 是库的物理组织,而命名空间是逻辑组织,其构成应与程序集的组织无关。命名空间可以且经常跨越多个程序集。
一定要为程序集 DLL 选择指示大的功能块(如 System.Data)的名称。程序集和 DLL 的名称不必对应于命名空间名称,但是在命名程序集时遵循命名空间名称这种做法是合理的。
考虑按下面的模式命名 DLL:
<Company>.<Component>.dll
其中 <Component> 包含一个或多个以圆点分隔的子句。
例如,Contoso.WebControls.dll。
1.4命名空间的名称
为命名空间选择的名称应指示命名空间中的类型所提供的功能。例如,System.Net.Sockets 命名空间包含的类型允许开发人员使用套接字通过网络进行通信。
命名空间名称的一般格式如下:
<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]
例如,Microsoft.WindowsMobile.DirectX。
使用公司名称作为命名空间的前缀以防止不同公司开发的命名空间具有相同的名称和前缀。在命名空间名称的第二级使用稳定的、与版本无关的产品名称。不要根据组织层次结构确定命名空间层次结构中的名称,因为公司的部门名称经过一段时间后可能会改变。命名空间名称是长期使用的、不会更改的标识符。组织的不断发展和变化不应使命名空间名称过时。
使用 Pascal 大小写格式,并用句点分隔命名空间各部分(如 Microsoft.Office.PowerPoint)。如果您的品牌采用了非传统的大小写,应遵循您的品牌所定义的大小写,即使它与常用的命名空间大小写相背离也如是。
适当的时候可考虑使用复数命名空间名称。例如,使用 System.Collections 而不使用 System.Collection。但是,品牌名称和首字母缩写词属于此规则的例外情况。例如,使用 System.IO,而不要使用 System.IOs。
命名空间和其中的类型不要使用相同的名称。例如,不要在将“Debug”用作命名空间名称的同时,又在该命名空间中提供一个名为“Debug”的类。有些编译器要求对这种类型进行完全限定。
命名空间一般准则
不要引入宽泛的类型名称,如 Element、Node、Log 和 Message。在通常情况下,这样极可能导致类型名称冲突。应对宽泛的类型名称进行限定(例如 FormElement、XmlNode EventLog、SoapMessage)。
应用程序命名空间准则
不要在单个应用程序模型内为命名空间中的多个类型指定相同的名称。
例如,如果要编写 Windows 窗体应用程序开发人员要使用的特殊控件库,则不应引入名为 Checkbox 的类型,因为该应用程序模型已存在同名类型 (CheckBox)。
1.5类、结构和接口的名称
通常,类型名称应该是名词短语,其中名词是由类型表示的实体。例如,Button、Stack 和 File 都具有名称,用于标识由类型表示的实体。从开发人员的角度选择标识实体的名称;名称应反映使用场合。
下面的准则适用于如何选择类型名称。
按照 Pascal 大小写格式,使用名词或名词短语(或偶尔使用形容词短语)为类、接口和值类型命名。
不要为类名加前缀(如字母 C)。
接口不适用此规则,它应以字母 I 开头。
考虑在派生类的末尾使用基类名称。
例如,从 Stream 继承的 Framework 类型以 Stream 结尾,从 Exception 继承的类型以 Exception 结尾。
为接口名称加上字母 I 前缀,以指示该类型为接口。
在定义类/接口对(其中类是接口的标准实现)时,一定要确保类和接口的名称除接口名称以字母 I 为前缀外,二者应完全相同。例如,Framework 提供 IAsyncResult 接口和 AsyncResult 类。
泛型类型参数的名称
泛型是 .NET Framework 2.0 版的主要新功能。下面的准则适用于为泛型类型参数选择正确的名称。
用描述性名称为泛型类型参数命名,除非单个字母的名称已完全可以自我说明而无需描述性名称。IDictionary 是一个符合此准则的接口的示例。
对具有一个单字母类型参数的类型,考虑将字母 T 用作这些类型的类型参数名称。
将字母 T 作为描述性类型参数名称的前缀。
考虑在参数名称中指示置于类型参数上的约束。例如,约束于 ISession 的参数可称为 TSession。
常见类型的名称
下面的准则提供的命名约定有助于开发人员了解某些类的使用场合。准则中提及的从某个其他类型继承的类型,指的是所有的继承者,而不只是直接继承的类型。例如,“向从 Exception 继承的类型添加 Exception 后缀”这一准则意味着在继承层次结构中具有 Exception 的任何类型都应该使用以 Exception 结尾的名称。
每条这样的准则还用来保留指定的后缀;除非类型满足该准则表述的条件,否则不应使用该后缀。例如,如果类型不是从 Exception 直接或间接继承的,则类型名称不能以 Exception 结尾。
向自定义属性类添加 Attribute 后缀。ObsoleteAttribute 和 AttributeUsageAttribute 是符合此准则的类型名称。
向在事件中使用的类型(如 C# 事件的返回类型)的名称添加 EventHandler 后缀。
AssemblyLoadEventHandler 是符合此准则的委托名称。
向不是事件处理程序的委托的名称添加 Callback 后缀。
不要向委托添加 Delegate 后缀。
向扩展 System.EventArgs 的类添加 EventArgs 后缀。
不要从 System.Enum 类派生;使用当前所用语言支持的关键字。例如,在 C# 中应使用 enum 关键字。
向从 System.Exception 继承的类型添加 Exception 后缀。
向实现 System.Collections.IDictionarystem.Collections.Generic. IDictionary<TKey, TValue> 的类型添加 Dictionary 后缀。注意,System.Collections.IDictionary 是特定类型的集合,但此准则的优先级高于以下更为一般的集合准则。
向实现 System.Collections.IEnumerable、System.Collections.ICollection、System.Collections.IList、System.Collections.Generic.IEnumerable<T>、System.Collections.Generic.ICollection<T> 或 System.Collections.Generic.IList<T> 的类型添加 Collection 后缀。
向从 System.IO.Stream 继承的类型添加 Stream 后缀。
向从 System.Security.CodeAccessPermission 继承的类型或实现 System.Security.IPermission 的类型添加 Permission 后缀。
枚举的名称
不要在枚举值名称中使用前缀。例如,不要对 ADO 枚举使用 ad 之类的前缀,也不要对多格式文本枚举使用 rtf 之类的前缀,依此类推。
这还意味着不应在枚举值名称中包含枚举类型名称。下面的代码示例演示了不正确的枚举值命名。
public enum Teams
{
TeamsAlpha,
TeamsBeta,
TeamsDelta
}
不要将 Enum 用作枚举类型的后缀。
不要在标志枚举的名称中添加 Flags 作为后缀。
对枚举使用单数名称,除非枚举值是位域。
对使用位域值的枚举(也称为标志枚举)使用复数名称。
1.6类型成员的名称
类型包含以下几种成员:
方法 属性 字段 事件
本节中的准则有助于类库设计者为成员选择与 .NET Framework 一致的名称。
方法的名称
使用动词或动词短语作为方法的名称。
通常,方法对数据进行操作,因此,使用动词描述方法的操作可使开发人员更易于了解方法所执行的操作。定义由方法执行的操作时,应从开发人员的角度仔细选择明确的名称。不要选择描述方法如何执行其操作的动词,也就是说,不要使用实现细节作为方法名称。
属性的名称
使用名词、名词短语或形容词作为属性的名称。
名词短语或形容词适合于属性,因为属性保存数据。
不要使用与 Get 方法同名的属性。
例如,不要将一个属性命名为 EmployeeRecord,又将一个方法命名为 GetEmployeeRecord。开发人员会不知道使用哪个成员来完成其编程任务。
使用肯定性短语作为布尔值属性的名称(如使用 CanSeek 而不使用 CantSeek)。此外,还可以为布尔值属性添加前缀(如 Is、Can 或 Has),但要注意使用得当。
考虑为属性提供与其类型相同的名称。
如果某个属性已强类型化为某个枚举,则该属性可与该枚举同名。例如,如果有一个名为 CacheLevel 的枚举,则返回其中一个枚举值的属性也可以命名为 CacheLevel。
事件的名称
使用动词或动词短语作为事件的名称。
在为事件命名时,使用现在时或过去时表示时间上的前后概念。例如,在窗口关闭之前引发的关闭事件可命名为“Closing”,在窗口关闭之后引发的关闭事件可命名为“Closed”。
不要使用“Before”或“After”作为前缀或后缀来指示之前和之后发生的事件。
使用后缀 EventHandler 命名事件处理程序(用作事件类型的委托)。
在事件处理程序签名中使用命名为“sender”和“e”的两个参数。
sender 参数的类型应为 Object,e 参数应是 EventArgs 的实例或继承自 EventArgs 的实例。
使用 EventArgs 后缀命名事件参数类。
字段的名称
字段的命名准则适用于静态公共字段和静态受保护字段。不要定义公共实例字段或受保护实例字段。有关更多信息,请参见字段设计。
在字段名称中使用 Pascal 大小写格式。
使用名词或名词短语作为字段的名称。
不要在字段名称中使用前缀。例如,不要使用 g_ 或 s_ 来区分静态字段和非静态字段。
1.7参数名
选择适当的参数名称可极大改善库的可用性。适当的参数名称应指示该参数会影响的数据或功能。
对参数名称使用 Camel 大小写。
使用描述性参数名称。
在大多数情况下,参数名称及其类型应足以确定参数的用法。
考虑使用反映参数含义的名称而不是反映参数类型的名称。
在开发人员工具和文档中,参数的类型通常都是可见的。通过选择一个说明参数的用法或含义的名称,可以向开发人员提供有价值的信息,帮助他们找到任务所需的成员,也有助于向成员传递正确的数据。
1.8资源的名称
本主题中准则适用于可本地化的资源,如错误信息和菜单文本。
在资源键中使用 Pascal 大小写格式。
提供描述性标识符,而不要提供短标识符。尽量保持标识符的简洁性,但不要牺牲可读性。
不要使用公共语言运行库 (CLR) 编程语言中特定于语言的关键字。
在命名资源中只能使用字母数字字符和下划线。
使用点分隔符(“.”)以清晰的层次结构表示标识符。
例如,Menus.FileMenu.Close.Text 和 Menus.FileMenu.Close.Color 等名称符合此准则。
对异常消息资源使用下面的命名约定。资源标识符应由异常类型名称加上异常的短标识符构成,二者之间以点分隔。例如,ArgumentException.BadEnumValue 符合此准则。
第二部分 类型设计准则
2.1类型和命名空间
下列准则可帮助您组织类型和命名空间,以便可以方便地查找和使用它们。
使用命名空间将类型组织到相关功能区域的层次结构中。
避免使用非常深的命名空间层次结构。这样的层次结构难于浏览,因为用户需要频繁沿层次结构反向移动。
避免使用过多的命名空间。
应将在同一方案中使用的类型尽可能放在同一命名空间中。用户在开发常见方案时,不应需要导入很多的命名空间。
避免将设计用于高级方案的类型与设计用于常见编程任务的类型放入同一命名空间中。
一般情况下,应将高级类型放入一般命名空间内的某个命名空间中,并将 Advanced 用作该命名空间的名称的最后一个标识符。例如,与 XML 序列化相关的常用类型位于 System.Xml.Serialization 命名空间中,而高级类型则位于 System.Xml.Serialization.Advanced 命名空间中。
定义类型时要指定类型的命名空间。
未指定命名空间的类型放在全局命名空间中。由于全局命名空间中的类型未在特定于功能的命名空间中,因此使用开发工具很难找到这些类型。
2.2在类和结构之间选择
类是引用类型,而结构是值类型。引用类型在堆中分配,内存管理由垃圾回收器处理。值类型在堆栈上或以内联方式分配,且在超出范围时释放。通常,值类型的分配和释放开销更小。然而,如果在要求大量的装箱和取消装箱操作的情况下使用,则值类型的表现就不如引用类型。有关更多信息,请参见装箱和取消装箱(C# 编程指南)。
如果类型的实例不大,且通常生存期短或嵌入其他对象,则考虑定义结构而不是类。
不要定义结构,除非该类型具备以下所有特征:
它在逻辑上表示单个值,与基元类型(整型、双精度型等)类似。
它的实例大小小于 16 字节。
它是不可变的。
它将不必频繁被装箱。
如果这些条件中的一个或多个没有满足,则创建引用类型而不是结构。不遵守此准则会对性能产生负面影响。
2.3在类和接口之间选择
接口定义实施者必须提供的一组成员的签名。接口不能提供成员的实现细节。例如,ICollection 接口定义与使用集合相关的成员。实现该接口的每个类都必须提供这些成员的实现细节。类可以实现多个接口。
类定义每个成员的成员签名和实现细节。Abstract(在 Visual Basic 中为 MustInherit)类的行为在某方面与接口或普通类相同,即可以定义成员,可以提供实现细节,但并不要求一定这样做。如果抽象类不提供实现细节,从该抽象类继承的具体类就需要提供实现。
虽然抽象类和接口都支持将协定与实现分离开来,但接口不能指定以后版本中的新成员,而抽象类可以根据需要添加成员以支持更多功能。
优先考虑定义类,而不是接口。
在库的以后版本中,可以安全地向类添加新成员;而对于接口,则只有修改现有代码才能添加成员。
请使用抽象(在 Visual Basic 中为 MustInherit)类,而不要使用接口来分离协定与实现。
如果需要提供多态层次结构的值类型,则应定义接口。
值类型必须从 ValueType 继承,并且只能从 ValueType 继承,因此值类型不能使用类来分离协定和实现。这种情况下,如果值类型要求多态行为,则必须使用接口。
请考虑定义接口来达到类似于多重继承的效果。
如果一个类型必须实现多个协定,或者协定适用于多种类型,请使用接口。例如,IDisposable 是由许多不同情况下使用的类型实现的。如果要求从基类继承的类可处置,会使类层次结构很不灵活。MemoryStream 等应从其父类继承基于流的协定的类,不可能还是可处置的。
2.4抽象类设计
任何情况下,抽象类都不应进行实例化,因此,正确定义其构造函数就非常重要。确保抽象类功能的正确性和扩展性也很重要。下列准则有助于确保抽象类能够正确地设计并在实现后可以按预期方式工作。
不要在抽象类型中定义公共的或受保护的内部(在 Visual Basic 中为 Protected Friend)构造函数。
具有 public 或 protected internal 可见性的构造函数用于能进行实例化的类型。任何情况下抽象类型都不能实例化。
应在抽象类中定义一个受保护构造函数或内部构造函数。
如果在抽象类中定义一个受保护构造函数,则在创建派生类的实例时,基类可执行初始化任务。内部构造函数可防止抽象类被用作其他程序集中的类型的基类。
对于您提供的每个抽象类,至少应提供一个具体的继承类型。
这样有助于库设计者在设计抽象类时找到问题所在。同时意味着开发人员在进行高级别开发时,即使不了解抽象类和继承,也可以使用具体类而不必学习这些概念。例如,.NET Framework 提供抽象类 WebRequest 向统一资源标识符发送请求,使用 WebResponse 接收统一资源标识符的回复。Framework 提供了 HttpWebRequest 和 HttpWebResponse 类,分别作为这两个抽象类的几个具体实现之一,它们是相应抽象类的 HTTP 特定的实现。
2.5静态类设计
静态类只包含从 Object 继承的实例成员,也没有可调用的构造函数。下面的准则有助于确保正确设计静态类。
请慎用静态类。
不要认为静态类可无所不包。
Environment 类使用静态类的方式值得学习。此类提供对当前用户环境的信息的访问。
不要声明或重写静态类中的实例成员。
如果某个类设计了实例成员,则该类不应标记为静态的。
如果编程语言没有对静态类的内置支持,则应将静态类声明为密封的和抽象的,并添加一个私有实例构造函数。
2.6接口设计
接口定义实施者必须提供的一组成员的签名。接口不能提供成员的实现细节。例如,ICollection 接口定义与使用集合相关的成员。实现该接口的每个具体类都必须提供这些成员的实现细节。虽然类只能从单个类继承,但可以实现多个接口。下面的准则有助于确保正确设计接口。
如果一组包含某些值类型的类型需要支持某些常用功能,则必须定义接口。
值类型必须从 ValueType 继承。因此,抽象类不能用于指定值类型的协定;而必须改用接口。
如果从某些其他类型继承的类型需要支持其功能,则考虑定义接口。
避免使用标记接口(没有成员的接口)。
自定义属性提供了一种标记类型的方式。有关自定义属性的更多信息,请参见编写自定义属性。如果可以将属性检查推迟到执行代码时才进行,则首选自定义属性。如果需要进行编译时检查,则不能使用此准则。
请提供至少一种接口实现的类型。
这样有助于确保正确设计和顺利实现接口。Int32 类提供 IComparable 接口的一个实现。
对于定义的每个接口,请提供至少一个使用该接口的成员(例如,采用该接口作为参数的方法,或类型化为接口的属性)。这是另一种有助于确保正确设计和顺利使用接口的机制。
不要向以前提供的接口添加成员。
添加新成员需要修改实现以前版本的接口的代码。这就是为什么在可能的情况下,通常首选使用类而不是接口的主要原因之一。
2.7枚举设计
枚举提供成组的常数值,它们有助于使成员成为强类型以及提高代码的可读性。枚举分为简单枚举和标志枚举两种。简单枚举包含的值不用于组合,也不用于按位比较。标志枚举应使用按位 OR 操作进行组合。标志枚举值的组合使用按位 AND 操作检查。
下列指南介绍了枚举设计的最佳做法。
一定要使用枚举强类型化参数、属性和表示值集的返回值。
一定要优选使用枚举而不是静态常量。
下面的代码示例演示了不正确的设计。
public static class BadFurnishings
{
public static int Table = 1;
public static int Chair = 2;
public static int Lamp = 3;
}
下面的代码示例演示应使用枚举来代替静态常量。
public enum GoodFurnishings
{
Table,
Chair,
Lamp
}
不要对开放集(如操作系统版本)使用枚举。
向已提供的枚举添加值会中断现有代码。有时可以接受这种做法,但不应在可能出现这种情况的场合设计枚举。
不要定义供将来使用的保留枚举值。
某些情况下,您可能认为为了向提供的枚举添加值,值得冒可能中断现有代码的风险。还可以定义使用其值的新的枚举和成员。
避免公开只有一个值的枚举。
一定不要将 sentinel 值包括在枚举中。
Sentinel 值用于标识枚举中的值的边界。通常,sentinel 值用于范围检查,它不是一个有效的数据值。下面的代码示例定义一个带有 sentinel 值的枚举。
public enum Furniture
{
Desk,
Chair,
Lamp,
Rug,
LastValue // The sentinel value.
}
一定要在简单枚举中提供一个零值。
如果可能,将此值命名为 None。如果 None 不适合,请将零值赋给最常用的值(默认值)。
考虑将 System.Int32(大多数编程语言的默认数据类型)用作枚举的基础数据类型,除非出现以下任何一种情况:
枚举是标志枚举,且您有 32 个以上的标志或者期望在将来有更多的标志。
基础类型需要与 Int32 不同,以便易于与期望不同大小的枚举的非托管代码进行互操作。
较小的基础类型可以节省大量空间。如果期望枚举主要用作控制流的参数,其大小就不太重要。如果出现下面的情况,大小节省可能会很重要:
期望枚举被用作非常频繁地实例化的结构或类中的字段。
期望用户创建枚举实例的大型数组或集合。
预计要序列化大量枚举实例。
一定要以名词或名词词组的复数来命名标志枚举。简单枚举应以单数的名词或名词词组命名。
不要直接扩展 System.Enum。一些编译器不允许扩展 Enum,除非间接地使用生成枚举的语言特定的关键字来进行扩展。
第三部分成员设计准则
3.1成员重载
成员的签名包含成员的名称和参数列表。每个成员签名在类型中必须是唯一的。只要成员的参数列表不同,成员的名称可以相同。如果类型的两个或多个成员是同类成员(方法、属性、构造函数等),它们具有相同的名称和不同的参数列表,则称该同类成员进行了重载。例如,Array 类包含两个 CopyTo 方法。第一个方法采用一个数组和一个 Int32 值,第二个方法采用一个数组和一个 Int64 值。
重载成员仅是是对同一功能提供有所不同的不同。例如,某个类型具有两个 CopyTo 成员,其中第一个成员向数组复制数据,第二个成员向文件复制数据,这样是不正确的。对成员进行重载通常是为了提供带少量参数或不带参数且易于使用的重载。这些成员调用功能更强大、要求经验丰富才能正确使用的重载。易于使用的重载通过向复杂重载传递默认值,支持常见的方案。例如,File 类提供 Open 方法的重载。简单重载 Open 采用文件路径和文件模式作为参数。它调用具有路径、文件模式、文件访问和文件共享参数的 Open 重载,并为文件访问和文件共享参数提供常用的默认值。如果开发人员不需要复杂重载所具有的灵活性,则不必了解文件访问和共享模型就可以打开文件。
为了便于维护和版本控制,简单重载应使用复杂重载来执行操作;基础功能不应在多个位置实现。
下面的准则有助于确保正确设计重载成员。
尽量使用描述性参数名称指示简单重载所使用的默认值。
此准则尤其适用于 Boolean 参数。复杂重载的参数名称应通过描述相反的状态或操作来指示简单重载所提供的默认值。例如,String 类提供下面的重载:
public static int Compare(string strA, string strB);
public static int Compare(string strA, string strB, bool ignoreCase);第二个重载提供一个名为 ignoreCase 的 Boolean 参数。即简单重载区分大小写,仅当要忽略大小写时,才需要使用复杂重载。默认值通常应为 false。
避免随意更改重载中的参数名称。如果某个重载的一个参数与另一个重载的一个参数表示相同的输入,则这两个参数应具有同样的名称。
例如,不要执行下面的操作:
public void Write(string message, FileStream stream){}
public void Write(string line, FileStream file, bool closeStream){}
这些重载的正确定义如下所示:
public void Write(string message, FileStream stream){}
public void Write(string message, FileStream stream, bool closeStream){}
保持重载成员参数的顺序一致性。在所有重载中,同名参数的位置应该相同。
public void Write(string message, FileStream stream){}
public void Write(FileStream stream, string message, bool closeStream){}
这些重载的正确定义如下所示:
public void Write(string message, FileStream stream){}
public void Write(string message, FileStream stream, bool closeStream){}
此准则有两项约束:
如果重载采用变量参数列表,则该列表必须是最后一个参数。
如果重载采用 out 参数,按照约定,这类参数应作为最后的参数。
如果需要具有扩展性,将最长的重载作为虚重载。较短的重载只应逐步调用较长的重载。
下面的代码示例对此进行了演示。
public void Write(string message, FileStream stream)
{
this.Write(message, stream, false);
}
public virtual void Write(string message, FileStream stream, bool closeStream)
{
// Do work here.
}
不要对重载成员使用 ref 或 out 修饰符。
public void Write(string message, int count)
public void Write(string message, out int count)
通常,如果设计中出现这种情况,则很可能存在更深层的设计问题。考虑是否应该重命名某个成员,以便提供关于方法执行的确切操作的更多信息。
使用成员重载而不要用默认参数定义成员。默认参数不符合 CLS,不能在某些语言中使用。
3.2在属性和方法之间选择
通常,方法代表操作而属性代表数据。属性应像字段一样使用,这意味着属性不应进行复杂的计算,也不应产生副作用。在不违反下列准则的情况下,应考虑使用属性而不是方法,因为属性对于经验较少的开发人员更易于使用。
如果成员表示类型的逻辑属性 (Attribute),请考虑使用属性 (Property)。
例如,BorderStyle 是一个属性 (Property),因为边框样式是 ListView 的属性 (Attribute)。
如果属性值存储在进程内存中并且该属性只是用于提供对值的访问,则要使用属性而不是方法。
在下列情况下要使用方法而不是属性。
操作比字段集慢数个数量级。即使考虑提供异步版本的操作来避免阻止线程,该操作也很可能因开销太大而不能使用属性。特别是,访问网络或文件系统(一次性初始化除外)的操作最可能是方法,而不是属性。
操作是转换,如 Object.ToString method。
操作在每次调用时都返回不同的结果,即使参数不发生更改也是如此。例如,NewGuid 方法在每次调用时都返回不同的值。
操作具有很大的显而易见的副作用。注意,一般不将填充内部缓存视为是显而易见的副作用。
操作返回内部状态的副本(这不包括在堆栈上返回的值类型对象的副本)。
操作返回一个数组。
3.3属性设计
下列准则可帮助确保正确地设计属性。
如果调用方不应当更改属性值,则要创建只读属性。
注意,属性类型的可变性会影响最终用户可以更改的内容。例如,如果定义一个返回读/写集合的只读属性,则最终用户不能向该属性分配其他集合,但可以修改该集合中的元素。
不要提供仅支持 Set 操作的属性。
如果无法提供属性 getter,可以改用一个方法来实现该功能。方法名称应以 Set 开头,并按原样后跟属性名。例如,AppDomain 使用一个名为 SetCachePath 的方法,而不是名为 CachePath 的仅支持 Set 操作的属性。
为所有属性提供适当的默认值,确保属性的默认值不会导致安全漏洞或设计效率非常低下。
允许按任意顺序设置属性,即便这样做会导致出现暂时无效的对象状态也如此。
如果属性 setter 引发异常,则保留以前的值。
避免从属性 getter 中引发异常。
属性 getter 应是没有任何前提条件的简单操作。如果 getter 可能会引发异常,请考虑将该属性重新设计为方法。此项建议不适用于索引器。索引可以因参数无效而引发异常。
在属性 setter 中引发异常是有效并可以接受的
3.4构造函数设计
构造函数是一类特殊的方法,用于初始化类型和创建类型的实例。类型构造函数用于初始化类型中的静态数据。类型构造函数由公共语言运行库 (CLR) 在创建类型的任何实例之前调用。类型构造函数是 static(在 Visual Basic 中为 Shared)方法,不能带任何参数。实例构造函数用于创建类型的实例。实例构造函数可以带参数,也可以不带参数。不带任何参数的实例构造函数称为默认构造函数。
下列准则描述了创建构造函数的最佳做法。
考虑提供简单的构造函数,最好是默认构造函数。简单构造函数的参数很少,并且所有参数都是基元类型或枚举。
如果所需操作的语义未直接映射到新实例的构造,或者按照构造函数设计准则是不合理的,则考虑使用静态工厂方法而不要使用构造函数。
将构造函数参数用作设置主要属性的快捷方式。
如果构造函数参数只用于设置一个属性,请务必为构造函数参数和该属性使用相同的名称。这类参数和属性之间的唯一差异应是大小写不同。
尽量减少构造函数中的任务。除了获取构造函数参数之外,构造函数不应执行太多操作。任何其他处理都应延迟到必要时再进行。
根据需要,可在实例构造函数中引发异常。
构造函数与其他方法一样,应引发并处理异常。具体地说,构造函数不应捕捉和隐藏它无法处理的任何异常。有关异常的更多信息,请参见异常设计准则。
如果需要公共默认构造函数,请在类中进行显式声明。
如果类支持默认构造函数,则显式定义默认构造函数是最佳做法。尽管某些编译器会自动向类中添加默认构造函数,但显式添加默认构造函数会使代码更易于维护。即使由于您添加了带参数的构造函数,导致编译器停止发出默认构造函数,这样也可确保定义默认构造函数。
不要在对象的构造函数中调用对象的虚成员。
无论是否调用了定义派生程度最高的重写的类型的构造函数,调用虚成员都会导致调用派生程度最高的重写。下面的代码示例对此进行了演示。基类构造函数执行时,即使尚未调用派生类的构造函数,也会调用派生类的成员。此示例输出 BadBaseClass 以显示 DerivedFromBad 构造函数尚未更新状态字段。
using System;
namespace Examples.DesignGuidelines.MemberDesign
{
public class BadBaseClass
{
protected string state;
public BadBaseClass()
{
state = "BadBaseClass";
SetState();
}
public virtual void SetState()
{
}
}
public class DerivedFromBad : BadBaseClass
{
public DerivedFromBad()
{
state = "DerivedFromBad ";
}
public override void SetState()
{
Console.WriteLine(state);
}
}
public class tester
{
public static void
{
DerivedFromBad b = new DerivedFromBad();
}
}
}
3.5字段设计
字段保存对象的关联数据。在大多数情况下,库中的所有非静态字段对开发人员都应是不可见的。下面的准则有助于在库设计中正确使用字段。
不要提供公共的或受保护的实例字段。
对不会更改的常数使用常数字段。
编译器将 const 字段的值直接插入调用代码中,这意味着任何情况下,const 值都不能更改,可避免引起兼容性问题。
对预定义对象实例使用公共静态只读字段。
例如,DateTime 类提供静态只读字段,使用这些字段可以获取设置为最大或最小时间值的 DateTime 对象。请参见 MaxValue 和 MinValue。
不要将可变类型的实例指定给只读字段。
使用可变类型创建的对象可以在创建后进行修改。例如,数组和大多数集合是可变类型,而 Int32、Uri 和 String 是不可变类型。对于保存可变引用类型的字段,只读修饰符可防止字段值被改写,但不能防止可变类型被修改。
下面的代码示例演示使用只读字段会出现的问题。BadDesign 类创建一个只读字段,并使用只读属性公开该字段。这不能防止 ShowBadDesign 类修改该只读字段的内容。
using System;
namespace Examples.DesignGuidelines.Fields
{
public class BadDesign
{
public readonly int[] data = {1,2,3};
public int [] Data
{
get {return data;}
}
public void WriteData()
{
foreach (int i in data)
{
Console.Write ("{0} ", i);
}
Console.WriteLine();
}
}
public class ShowBadeDesign
{
public static void
{
BadDesign bad = new BadDesign();
// The following line will write: 1 2 3
bad.WriteData();
int[] badData = bad.Data;
for (int i = 0; i< badData.Length; i++)
{
badData[i] = 0;
}
// The following line will write: 0 0 0
// because bad's data has been modified.
bad.WriteData();
}
}
}
3.6参数设计
3.6.1在枚举和布尔参数之间选择
下列准则可帮助您确定参数类型应是枚举还是 Boolean 值。
如果成员本来有两个或多个布尔参数,则要使用枚举。
除非完全确定永不需要多于两个的值,否则不要使用布尔值。
3.6.2验证参数
下列准则有助于确保正确验证参数。
一定要验证传递给公共的、受保护的或显式实现的成员的参数。如果验证失败,则引发 System.ArgumentException 或其派生类之一。
此准则不要求验证代码在公共可见成员中。将参数传递给处理验证的内部方法是可以接受的。
如果传递了一个空参数而成员不支持空(在 Visual Basic 中为 Nothing)参数,则应引发 System.ArgumentNullException。
一定要验证枚举参数。
不能假设枚举参数为在枚举中定义的值,因为公共语言运行库 (CLR) 支持将任何整数值强制转换为枚举值,而不论该值是否在枚举中定义。
应注意传递的可变参数在经过验证后可能已发生了更改。
如果成员是安全敏感的,则制作可变对象的私有副本并使用该副本进行验证和处理。这仅适用于可变数据。不需要复制不可变数据(如 Uri 对象)。
3.6.3传递参数
方法参数可以通过值,通过引用传递,也可以作为输出参数传递。通过值传递参数时,方法获取调用方数据的副本,但不能改变调用方的数据副本。通过引用传递参数时,方法获取指向调用方数据的指针。此数据与调用方共享。如果方法对引用参数做出更改,则这些更改是对调用方的数据进行的。使用引用参数时,方法可以使用数据的初始状态。输出参数与引用参数类似,不同之处在于,输出参数以独占方式用于向调用方返回数据,而引用参数可用于将数据传入方法,也可用于从方法中接收数据。
避免使用输出参数或引用参数。
使用定义输出参数或引用参数的成员需要开发人员理解指针、值类型和引用类型之间的细微差别以及输出参数和引用参数之间的初始化差异。
不要通过引用传递引用类型。
通过引用传递一个对象使方法能够用不同的实例替换该对象。在大多数情况下,方法应使用提供的对象,而不应将其替换。对于此规则,有一些少量的例外(例如,可用于交换引用的方法)。
第四部分异常设计准则
异常是报告错误的标准机制。应用程序和库不应使用返回代码来传递错误信息。异常的采用增进了框架设计的一致性,允许无返回类型的成员(如构造函数)报告错误。异常还允许程序处理错误或根据需要终止运行。默认行为是在应用程序不处理引发的异常时,终止应用程序。
4.1异常引发
当某一成员无法成功执行它应执行的操作时,将引发异常。这称为执行故障。例如,如果 Connect 方法无法连接到指定的远程终结点,则这就是一个执行故障,将有一个异常被引发。
下列准则可帮助确保在适当时引发异常。
不要返回错误代码。异常是报告框架中的错误的主要手段。
通过引发异常来报告执行故障。如果某一成员无法按预期方式成功执行,则应将这种情况视为一个执行故障并引发一个异常。
考虑引发异常的性能影响。
记录公共可调用的成员因成员协定冲突(而不是系统故障)而引发的所有异常,并将这些异常视为协定的一部分。包含在协定中的异常不应从一个版本更改到下一个版本。
不要包含可以根据某一选项引发或不引发异常的公共成员。
不要包含将异常作为返回值或输出参数返回的公共成员。
此项准则适用于公共可见的成员。使用私有帮助器方法构造和初始化异常是可以接受的。
考虑使用异常生成器方法。从不同的位置引发同一异常会经常发生。为了避免代码膨胀,请使用帮助器方法创建异常并初始化其属性。
避免从 finally 块中显式引发异常。可以接受因调用引发异常的方法而隐式引发的异常。
4.1.1选择要引发的正确异常类型
下列设计准则可帮助您确保正确地使用现有异常,并在适当的时候创建对您的库有价值的新异常。
考虑引发 System 命名空间中的现有异常,而不是创建自定义异常类型。
有关 .NET Framework 所提供的最常用异常类型的详细准则,请参见捕捉和引发标准异常类型。
如果错误状态可以通过不同于现有任何其他异常的方法以编程方式进行处理,则要创建并引发自定义异常。否则,引发一个现有异常。
不要只是为了您所在的团队获得异常而创建和引发新异常。
引发适当的最具体(派生程度最大)的异常。例如,如果某方法收到一个 null(在 Visual Basic 中为 Nothing)参数,则该方法应引发 System.ArgumentNullException,而不是引发该异常的基类型 System.ArgumentException。
4.1.2错误信息设计
下面的准则有助于确保异常消息有意义且格式正确。
在引发异常时为开发人员提供丰富且有意义的消息文本。消息应说明导致异常的原因并清楚描述避免该异常需采取的操作。
生成库时,消息应针对开发人员设计。
确保异常消息的语法正确。顶级异常处理程序可以向应用程序终端用户显示异常消息。
确保消息文本的每个句子都是以句号(“。”)结尾。这样,向用户显示异常消息的代码不必处理开发人员忘记最后面的句号的情况,这种处理相当麻烦而且代价很大。
避免在异常消息中使用问号(“?”)和感叹号(“!”)。
不要在不要求相应权限的异常消息中透露安全敏感信息。
有关安全库设计的更多信息,请参见编写安全类库。
如果希望使用不同语言的开发人员使用您的组件,则应考虑对您的组件所引发的异常消息进行本地化。
4.2异常处理
下面的准则有助于确保库正确处理异常。
不要在框架代码中捕捉非特定异常(如 System.Exception、System.SystemException 等)以至忽略错误。
如果捕捉异常是为了再次引发或传输给其他线程,则可以捕捉这些异常。下面的代码示例演示的异常处理是不正确的。
public class BadExceptionHandlingExample1
{
public void DoWork()
{
// Do some work that might throw exceptions.
}
public void MethodWithBadHandler()
{
try
{
DoWork();
}
catch (Exception e)
{
// Swallow the exception and continue
// executing.
}
}
}
避免在应用程序代码中捕捉非特定异常(如 System.Exception、System.SystemException 等)以至忽略错误。某些情况下,可以在应用程序中忽略错误,但这种情况极少。
如果捕捉异常是为了传输异常,则不要排除任何特殊异常。
只捕捉能够合法处理的异常,而不要在 catch 子句中创建特殊异常的列表。在非特定异常处理程序中,不能处理的异常不应视为特殊处理的特殊情况。
如果了解特定异常在给定上下文中引发的条件,请考虑捕捉这些异常。
应该只捕捉可以从中恢复的异常。例如,试图打开不存在的文件而导致的 FileNotFoundException 可以由应用程序处理,因为应用程序可以将问题传达给用户,并允许用户指定其他文件名或创建该文件。如果打开文件的请求会生成 ExecutionEngineException,则不应该处理该请求,因为没有任何把握可以了解该异常的基础原因,应用程序也无法确保继续执行是安全的。
不要过多使用 catch。通常应允许异常在调用堆栈中往上传播。
捕捉无法合法处理的异常会隐藏关键的调试信息。
使用 try-finally 并避免将 try-catch 用于清理代码。在书写规范的异常代码中,try-finally 比 try-catch 使用得更多。
使用 catch 子句是为了允许处理异常(例如,通过纪录非致命错误)。无论是否引发了异常,使用 finally 子句即可执行清理代码。如果分配了昂贵或有限的资源(如数据库连接或流),则应将释放这些资源的代码放置在 finally 块中。
4.3捕捉和引发标准异常类型
Exception 和 SystemException
不要引发 System.Exception 或 System.SystemException。
不要在框架代码中捕捉 System.Exception 或 System.SystemException,除非打算再次引发。
避免捕捉 System.Exception 或 System.SystemException,在顶级异常处理程序中除外。
ApplicationException
不要引发 System.ApplicationException 或从该异常进行派生。
InvalidOperationException
如果处于不适当的状态,则引发 System.InvalidOperationException 异常。如果没有向属性集或方法调用提供适当的对象当前状态,则应引发 System.InvalidOperationException。例如,向已打开用于读取的 System.IO.FileStream 写入时,应引发 System.InvalidOperationException 异常。
一组相关对象的组合状态对于操作无效时,也应引发此异常。
ArgumentException、ArgumentNullException 和 ArgumentOutOfRangeException
如果向成员传递了错误的参数,则引发 System.ArgumentException 或其子类型之一。如果适用,首选派生程度最高的异常类型。
在引发 System.ArgumentException 或其派生类型之一时,设置 System.ArgumentException.ParamName 属性。此属性存储导致引发异常的参数的名称。注意,使用其中一个构造函数重载可以设置该属性。
使用属性 setter 的隐式值参数的名称的值
不要允许公开可调用的 API 显式或隐式引发 System.NullReferenceException、System.AccessViolationException、System.InvalidCastException 或 System.IndexOutOfRangeException。进行参数检查以避免引发这些异常。引发这些异常会公开方法的实现细节,这些细节可能会随时间发生更改。
StackOverflowException
不要显式引发 System.StackOverflowException。此异常只应由公共语言运行库 (CLR) 显式引发。
不要捕捉 System.StackOverflowException。
以编程方式处理堆栈溢出极为困难。应允许此异常终止进程并使用调试确定问题的根源。
OutOfMemoryException
不要显式引发 System.OutOfMemoryException。此异常只应由 CLR 基础结构引发。
ComException 和 SEHException
不要显式引发 System.Runtime.InteropServices.COMException 或 System.Runtime.InteropServices.SEHException。这些异常只应由 CLR 基础结构引发。
不要显式捕捉 System.Runtime.InteropServices.SEHException。
ExecutionEngineException
不要显式引发 System.ExecutionEngineException。
4.4设计自定义异常
下列指南有助于确保正确设计您的自定义异常。
避免使用深的异常层次结构。
一定要从 System.Exception 或其他常见基本异常之一派生异常。
请注意,捕捉和引发标准异常类型具有一个指南,指出不应从 ApplicationException 派生自定义异常。
异常类名称一定要以后缀 Exception 结尾。
应使异常可序列化。异常必须可序列化才能跨越应用程序域和远程处理边界正确工作。
一定要在所有异常上都提供(至少是这样)下列常见构造函数。确保参数的名称和类型与在下面的代码示例中使用的那些相同。
public class NewException : BaseException, ISerializable
{
public NewException()
{
// Add implementation.
}
public NewException(string message)
{
// Add implementation.
}
public NewException(string message, Exception inner)
{
// Add implementation.
}
// This constructor is needed for serialization.
protected NewException(SerializationInfo info, StreamingContext context)
{
// Add implementation.
}
}
4.5异常和性能
引发异常可能会对性能造成不良的影响。对于经常性执行失败的代码,可以使用设计模式最大限度地减少性能问题。本主题描述两种设计模式,在异常可能会严重影响性能时使用这两种模式很有帮助。
不要由于担心异常可能会对性能造成不良影响而使用错误代码。
利用设计来减少性能问题。在本主题中描述了两种设计模式。
对于可能在常见方案中引发异常的成员,可以考虑使用 Tester-Doer 模式来避免与异常相关的性能问题。
Tester-Doer 模式将可能引发异常的调用划分为两部分:Tester 和 Doer。Tester 对可能导致 Doer 引发异常的状态执行测试。测试恰好插入在引发异常的代码之前,从而防范异常发生。
下面的代码示例演示此模式的 Doer 部分。该示例包含一个方法,在向该方法传递 null(在 Visual Basic 中为 Nothing)值时该方法将引发异常。如果频繁地调用该方法,就可能会对性能造成不良影响。
public class Doer
{
// Method that can potential throw exceptions often.
public static void ProcessMessage(string message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
}
// Other methods...
}
下面的代码示例演示此模式的 Tester 部分。该方法利用一个测试来避免在 Doer 将引发异常时调用 Doer (ProcessMessage)。
public class Tester
{
public static void TesterDoer(ICollection<string> messages)
{
foreach (string message in messages)
{
// Test to ensure that the call
// won't cause the exception.
if (message != null)
{
Doer.ProcessMessage(message);
}
}
}
}
注意,当在测试涉及可变对象的多线程应用程序中使用该模式时,必须解决可能出现的争用状态问题。线程可以在测试之后且 Doer 执行之前更改可变对象的状态。使用线程同步技术可以解决这些问题。
对于可能在常见方案中引发异常的成员,可以考虑使用 TryParse 模式来避免与异常相关的性能问题。
若要实现 TryParse 模式,需要为执行可在常见方案中引发异常的操作提供两种不同的方法。第一种方法 X, 执行该操作并在适当时引发异常。第二种方法 TryX, 不引发异常,而是返回一个 Boolean 值以指示成功还是失败。由对 TryX 的成功调用所返回的任何数据都通过使用 out(在 Visual Basic 中为 ByRef)参数予以返回。Parse 和 TryParse 方法就是此模式的示例。
为每个使用 TryParse 模式的成员提供一个引发异常的成员。
只提供 TryX 方法几乎没有什么时候是正确的设计,因为使用该方法需要了解 out 参数。此外,对于大多数常见方案来说,异常对性能的影响不会构成问题;因此应在大多数常见方案中提供易于使用的方法。
- Microsoft类库开发的设计准则
- Net 类库开发的设计准则
- Microsoft SQL 服务器的最佳实践,设计和开发准则
- Microsoft SQL 服务器的最佳实践,设计和开发准则
- 类库开发设计准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- Web开发设计的五大准则
- WIKI的设计准则
- 方法设计的准则
- 硬件设计的准则
- 非功能性需求
- 在Flex/ActionScript3中实现单例
- C# 接口(四 访问接口)
- 浅谈.NET中的数据绑定表达式(一)(转)
- 取汉字首字母的类, 支持多音字
- Microsoft类库开发的设计准则
- 两种利用内存流存取图片的经典例子
- linux操作系统下的域名解析系统设置
- NBear Presenter需要多个Service时的处理方法
- ASP.NET数据绑定与面页的嵌入式代码
- 嵌入式GUI与Qt/Embedded
- 用C#读取GPS数据的基类,适用于wince操作系统。 适合自带GPS模块的PDA或智能手机的设备
- Oracle/PLSQL: Foreign Keys
- classmethod,staticmethod,还有类里面一般的的method有什么区别