类库开发的设计准则 读书笔记 2类型设计准则
MSDN链接:http://msdn.microsoft.com/zh-cn/library/vstudio/ms229036(v=vs.100).aspx
系列文章列表:
1名称准则:http://www.cnblogs.com/liu-meng/p/4181984.html
2类型设计准则:http://www.cnblogs.com/liu-meng/p/4182737.html
类型与命名空间:
使用命名空间将类型组织到相关功能区域的的层次结构中
避免使用较深的命名空间层次结构
避免使用过多的命名空间
避免将设计用于高级方案的类型与设计用于常见编程任务的类型放到同一命名空间中,一般情况下,应将高级类型放入一般命名空间内的某个命名空间中,并将 Advanced 用作该命名空间的名称的最后一个标识符,例如,与 XML 序列化相关的常用类型位于System.Xml.Serialization 命名空间中,而高级类型则位于 System.Xml.Serialization.Advanced 命名空间中。
定义类型时要指定类型的命名空间。
在类和结构之间选择:
类是引用类型,而结构是值类型。 引用类型在堆中分配,内存管理由垃圾回收器处理。 值类型在堆栈上或以内联方式分配,且在超出范围时释放。 通常,值类型的分配和释放开销更小。 然而,如果在要求大量的装箱和取消装箱操作的情况下使用,则值类型的表现就不如引用类型。
如果类型的实例不大,且通常生存期短或嵌入其他对象,则考虑定义结构而不是类。
不要定义结构,除非该类型具备以下所有特征:
-
它在逻辑上表示单个值,与基元类型(整型、双精度型等)类似。
-
它的实例大小小于 16 字节。
-
它是不可变的。
-
它将不必频繁被装箱。
在类和接口之间选择:
接口定义实施者必须提供的一组成员的签名。 接口不能提供成员的实现细节。 实现该接口的每个类都必须提供这些成员的实现细节。 类可以实现多个接口。
类定义每个成员的成员签名和实现细节。 Abstract (在 Visual Basic 中为 MustInherit)类的行为在某方面与接口或普通类相同,即可以定义成员,可以提供实现细节,但并不要求一定这样做。 如果抽象类不提供实现细节,从该抽象类继承的具体类就需要提供实现。
虽然抽象类和接口都支持将协定与实现分离开来,但接口不能指定以后版本中的新成员,而抽象类可以根据需要添加成员以支持更多功能。
优先考虑定义类,而不是接口。在库的以后版本中,可以安全地向类添加新成员;而对于接口,则只有修改现有代码才能添加成员。
请使用抽象(在 Visual Basic 中为 MustInherit)类,而不要使用接口来分离协定与实现。如果需要提供多态层次结构的值类型,则应定义接口。值类型必须从 ValueType 继承,并且只能从 ValueType 继承,因此值类型不能使用类来分离协定和实现。 这种情况下,如果值类型要求多态行为,则必须使用接口。
请考虑定义接口来达到类似于多重继承的效果。如果一个类型必须实现多个协定,或者协定适用于多种类型,请使用接口。
抽象类设计:
不要在抽象类型中定义公共的或受保护的内部(在 Visual Basic 中为 Protected Friend)构造函数
应在抽象类中定义一个受保护构造函数或内部构造函数。如果在抽象类中定义一个受保护构造函数,则在创建派生类的实例时,基类可执行初始化任务。 内部构造函数可防止抽象类被用作其他程序集中的类型的基类。
对于您提供的每个抽象类,至少应提供一个具体的继承类型。
静态类的设计:
静态类只包含从 Object 继承的实例成员,也没有可调用的构造函数。
请慎用静态类。静态类只应用作面向对象的框架核心的支持类。
不要认为静态类可无所不包。Environment 类使用静态类的方式值得学习。 此类提供对当前用户环境的信息的访问。
不要声明或重写静态类中的实例成员。如果某个类设计了实例成员,则该类不应标记为静态的。
如果编程语言没有对静态类的内置支持,则应将静态类声明为密封的和抽象的,并添加一个私有实例构造函数。
接口的设计:
接口定义实施者必须提供的一组成员的签名。 接口不能提供成员的实现细节。 实现该接口的每个具体类都必须提供这些成员的实现细节。虽然类只能从单个类继承,但可以实现多个接口。
如果一组包含某些值类型的类型需要支持某些常用功能,则必须定义接口。
如果从某些其他类型继承的类型需要支持其功能,则考虑定义接口。
避免使用标记接口(没有成员的接口)。自定义特性提供了一种标记类型的方式。如果可以将特性检查推迟到执行代码时才进行,则首选自定义特性。 如果需要进行编译时检查,则不能使用此准则。
请提供至少一种接口实现的类型。
对于定义的每个接口,请提供至少一个使用该接口的成员(例如,采用该接口作为参数的方法,或类型化为接口的属性)。这是另一种有助于确保正确设计和顺利使用接口的机制。
不要向以前提供的接口添加成员。
结构的设计:
结构是值类型。 结构是在堆栈上或以内联方式分配的,当结构超出范围时将被释放。 通常情况下,值类型的内存空间分配和释放的开销较小;但在需要大量装箱和取消装箱操作的方案中,值类型的执行性能较引用类型要差。 有关更多信息,请参见装箱和取消装箱(C# 编程指南)。
不要为结构提供默认的构造函数。如果某一结构定义了默认构造函数,则在创建该结构的数组时,公共语言运行时会自动对每个数组元素执行该默认构造函数。有些编译器(如 C# 编译器)不允许结构拥有默认构造函数。
对值类型实现 System.IEquatable`1。在确定两个值类型是否相等时,IEquatable<T> 要优于 Equals。 通过使用接口,调用方可避免装箱和托管反射的不良性能影响。
不要显式扩展 System.ValueType。有些编译器不允许扩展 ValueType。
确保所有实例数据均设置为零、false 或 null(根据需要)的状态是无效的。如果遵循这一准则,新构造的值类型实例不会处于不可用的状态。
例如,下面的结构的设计是错误的。 参数化构造函数有意确保存在有效的状态,但在创建结构数组时不执行该构造函数。 这意味着实例字段 label 初始化为 null(在 Visual Basic 中为 Nothing),这对于此结构的 ToString 实现是无效的。
public struct BadStructure { string label; int width; int length; public BadStructure (string labelValue, int widthValue, int lengthValue) { if (labelValue == null || labelValue.Length ==0) { throw new ArgumentNullException("label"); } label = labelValue; width = widthValue; length = lengthValue; } public override string ToString() { // Accessing label.Length throws a NullReferenceException // when label is null. return String.Format("Label length: {0} Label: {1} Width: {2} Length: {3}", label.Length, label, width,length); } }
在下面的代码示例中,GoodStructure 的设计对 label 字段的状态未作任何假定。 ToString 方法设计为处理 null 标签。
public struct GoodStructure { string label; int width; int length; public GoodStructure (string labelValue, int widthValue, int lengthValue) { label = labelValue; width = widthValue; length = lengthValue; } public override string ToString() { // Handle the case where label might be // initialized to null; string formattedLabel = label; int formattedLableLength; if (formattedLabel == null) { formattedLabel = "<no label value specified>"; formattedLableLength = 0; } else { formattedLableLength = label.Length; } return String.Format("Label Length: {0} Label: {1} Width: {2} Length: {3}", formattedLableLength, formattedLabel, width, length); } }
枚举的设计:
枚举提供成组的常数值,它们有助于使成员成为强类型以及提高代码的可读性。 枚举分为简单枚举和标志枚举两种。 简单枚举包含的值不用于组合,也不用于按位比较。 标志枚举应使用按位 OR 操作进行组合。 标志枚举值的组合使用按位 AND 操作检查。
一定要使用枚举强类型化参数、属性和表示值集的返回值。
一定要优选使用枚举而不是静态常量。
不要对开放集(如操作系统版本)使用枚举。
不要定义供将来使用的保留枚举值。某些情况下,您可能认为为了向提供的枚举添加值,值得冒可能中断现有代码的风险。 还可以定义使用其值的新的枚举和成员。
避免公开只有一个值的枚举。
一定不要将 sentinel 值包括在枚举中。Sentinel 值用于标识枚举中的值的边界。 通常,sentinel 值用于范围检查,它不是一个有效的数据值。 下面的代码示例定义一个带有 sentinel 值的枚举。
public enum Furniture { Desk, Chair, Lamp, Rug, LastValue // The sentinel value. }
一定要在简单枚举中提供一个零值。如果可能,将此值命名为 None。 如果 None 不适合,请将零值赋给最常用的值(默认值)。
考虑将 System.Int32(大多数编程语言的默认数据类型)用作枚举的基础数据类型,除非出现以下任何一种情况:
-
-
枚举是标志枚举,且您有 32 个以上的标志或者期望在将来有更多的标志。
-
基础类型需要与 Int32 不同,以便易于与期望不同大小的枚举的非托管代码进行互操作。
-
较小的基础类型可以节省大量空间。 如果期望枚举主要用作控制流的参数,其大小就不太重要。 如果出现下面的情况,大小节省可能会很重要:
-
期望枚举被用作非常频繁地实例化的结构或类中的字段。
-
期望用户创建枚举实例的大型数组或集合。
-
预计要序列化大量枚举实例。
-
-
一定要以名词或名词词组的复数来命名标志枚举。 简单枚举应以单数的名词或名词词组命名。
不要直接扩展 System.Enum。
嵌套类型:
嵌套类型是作为某其他类型的成员的类型。 嵌套类型应与其声明类型紧密关联,并且不得用作通用类型。 有些开发人员会将嵌套类型弄混淆,因此嵌套类型不应是公开可见的,除非不得不这样做。 在设计完善的库中,开发人员几乎不需要使用嵌套类型实例化对象或声明变量。
在声明类型使用和创建嵌套类型实例时,嵌套类型很有用,但不在公共成员中公开嵌套类型的使用。
如果嵌套类型和其外部类型之间的关系需要成员可访问性语义,则要使用嵌套类型。由于嵌套类型被视为是声明类型的成员,因此嵌套类型可以访问声明类型中的所有其他成员。
不要将公共嵌套类型用作逻辑分组构造;请改用命名空间。
避免公开显露嵌套类型。 唯一的特例是需要声明嵌套类型的变量的情况,在生成子类或其他高级自定义等极少数情况下需要声明嵌套类型的变量。
如果可能在声明类型的外部引用类型,则不要使用嵌套类型。在常见方案中,不应要求对嵌套类型进行变量声明和对象实例化。 例如,处理在某一类上定义的事件的事件处理程序委托不应嵌套在该类中。
如果需要由客户端代码实例化类型,则不要使用嵌套类型。 如果某种类型具有公共构造函数,就可能不应进行嵌套。
不要将嵌套类型定义为接口的成员。 许多语言不支持这样的构造。