类库开发的设计准则-2-类型设计准则

本节提供的准则可帮助库设计人员从各种设计中做出选择,并正确实现类型。

类型和命名空间

下列准则可帮助您组织类型和命名空间,以便可以方便地查找和使用它们。

避免使用过多的命名空间。

应将在同一方案中使用的类型尽可能放在同一命名空间中。用户在开发常见方案时,不应需要导入很多的命名空间。

避免将设计用于高级方案的类型与设计用于常见编程任务的类型放入同一命名空间中。

一般情况下,应将高级类型放入一般命名空间内的某个命名空间中,并将 Advanced 用作该命名空间的名称的最后一个标识符。例如,与 XML 序列化相关的常用类型位于 System.Xml.Serialization 命名空间中,而高级类型则位于 System.Xml.Serialization.Advanced 命名空间中。

定义类型时要指定类型的命名空间。

未指定命名空间的类型放在全局命名空间中。由于全局命名空间中的类型未在特定于功能的命名空间中,因此使用开发工具很难找到这些类型。此外,全局命名空间中的名称冲突问题也无法解决。有关更多信息,请参见命名空间的名称

在类和结构之间选择

类是引用类型,而结构是值类型。引用类型在堆中分配,内存管理由垃圾回收器处理。值类型在堆栈上或以内联方式分配,且在超出范围时释放。通常,值类型的分配和释放开销更小。然而,如果在要求大量的装箱和取消装箱操作的情况下使用,则值类型的表现就不如引用类型。有关更多信息,请参见装箱和取消装箱(C# 编程指南)

有关值类型和引用类型的更多信息,请参见通用类型系统概述

不要定义结构,除非该类型具备以下所有特征:

  • 它在逻辑上表示单个值,与基元类型(整型、双精度型等)类似。
  • 它的实例大小小于 16 字节。
  • 它是不可变的。
  • 它将不必频繁被装箱。

如果这些条件中的一个或多个没有满足,则创建引用类型而不是结构。不遵守此准则会对性能产生负面影响。

在类和接口之间选择

接口定义实施者必须提供的一组成员的签名。接口不能提供成员的实现细节。例如,ICollection 接口定义与使用集合相关的成员。实现该接口的每个类都必须提供这些成员的实现细节。类可以实现多个接口。

类定义每个成员的成员签名和实现细节。Abstract(在 Visual Basic 中为 MustInherit)类的行为在某方面与接口或普通类相同,即可以定义成员,可以提供实现细节,但并不要求一定这样做。如果抽象类不提供实现细节,从该抽象类继承的具体类就需要提供实现。

虽然抽象类和接口都支持将协定与实现分离开来,但接口不能指定以后版本中的新成员,而抽象类可以根据需要添加成员以支持更多功能。

优先考虑定义类,而不是接口。

在库的以后版本中,可以安全地向类添加新成员;而对于接口,则只有修改现有代码才能添加成员。

如果需要提供多态层次结构的值类型,则应定义接口。

值类型必须从 ValueType 继承,并且只能从 ValueType 继承,因此值类型不能使用类来分离协定和实现。这种情况下,如果值类型要求多态行为,则必须使用接口。

请考虑定义接口来达到类似于多重继承的效果。

如果一个类型必须实现多个协定,或者协定适用于多种类型,请使用接口。例如,IDisposable 是由许多不同情况下使用的类型实现的。如果要求从基类继承的类可处置,会使类层次结构很不灵活。MemoryStream 等应从其父类继承基于流的协定的类,不可能还是可处置的。

抽象类设计

任何情况下,抽象类都不应进行实例化,因此,正确定义其构造函数就非常重要。确保抽象类功能的正确性和扩展性也很重要。下列准则有助于确保抽象类能够正确地设计并在实现后可以按预期方式工作。

不要在抽象类型中定义公共的或受保护的内部(在 Visual Basic 中为 Protected Friend)构造函数。

具有 public 或 protected internal 可见性的构造函数用于能进行实例化的类型。任何情况下抽象类型都不能实例化。

应在抽象类中定义一个受保护构造函数或内部构造函数。

如果在抽象类中定义一个受保护构造函数,则在创建派生类的实例时,基类可执行初始化任务。内部构造函数可防止抽象类被用作其他程序集中的类型的基类。

对于您提供的每个抽象类,至少应提供一个具体的继承类型。

这样有助于库设计者在设计抽象类时找到问题所在。同时意味着开发人员在进行高级别开发时,即使不了解抽象类和继承,也可以使用具体类而不必学习这些概念。例如,.NET Framework 提供抽象类 WebRequest 向统一资源标识符发送请求,使用 WebResponse 接收统一资源标识符的回复。Framework 提供了HttpWebRequest 和 HttpWebResponse 类,分别作为这两个抽象类的几个具体实现之一,它们是相应抽象类的 HTTP 特定的实现。

静态类设计

静态类只包含从 Object 继承的实例成员,也没有可调用的构造函数。下面的准则有助于确保正确设计静态类。

请慎用静态类。

静态类只应用作面向对象的框架核心的支持类。

不要认为静态类可无所不包。

Environment 类使用静态类的方式值得学习。此类提供对当前用户环境的信息的访问。

不要声明或重写静态类中的实例成员。

如果某个类设计了实例成员,则该类不应标记为静态的。

如果编程语言没有对静态类的内置支持,则应将静态类声明为密封的和抽象的,并添加一个私有实例构造函数。

接口设计

接口定义实施者必须提供的一组成员的签名。接口不能提供成员的实现细节。例如,ICollection 接口定义与使用集合相关的成员。实现该接口的每个具体类都必须提供这些成员的实现细节。虽然类只能从单个类继承,但可以实现多个接口。下面的准则有助于确保正确设计接口。

如果一组包含某些值类型的类型需要支持某些常用功能,则必须定义接口。

值类型必须从 ValueType 继承。因此,抽象类不能用于指定值类型的协定;而必须改用接口。

避免使用标记接口(没有成员的接口)。

自定义属性提供了一种标记类型的方式。有关自定义属性的更多信息,请参见编写自定义属性。如果可以将属性检查推迟到执行代码时才进行,则首选自定义属性。如果需要进行编译时检查,则不能使用此准则。

请提供至少一种接口实现的类型。

这样有助于确保正确设计和顺利实现接口。Int32 类提供 IComparable 接口的一个实现。

对于定义的每个接口,请提供至少一个使用该接口的成员(例如,采用该接口作为参数的方法,或类型化为接口的属性)。

这是另一种有助于确保正确设计和顺利使用接口的机制。

不要向以前提供的接口添加成员。

添加新成员需要修改实现以前版本的接口的代码。这就是为什么在可能的情况下,通常首选使用类而不是接口的主要原因之一。有关更多信息,请参见在类和接口之间选择

如果接口的交付定义要求更多成员,则可以实现新的接口和使用该接口的适当成员。

结构设计

结构是值类型。结构是在堆栈上或以内联方式分配的,当结构超出范围时将被释放。通常情况下,值类型的内存空间分配和释放的开销较小;但在需要大量装箱和取消装箱操作的方案中,值类型的执行性能较引用类型要差。有关更多信息,请参见装箱和取消装箱(C# 编程指南)

有关值类型和引用类型的更多信息,请参见通用类型系统概述

不要为结构提供默认的构造函数。

如果某一结构定义了默认构造函数,则在创建该结构的数组时,公共语言运行库会自动对每个数组元素执行该默认构造函数。

有些编译器(如 C# 编译器)不允许结构拥有默认构造函数。

对值类型实现 System.IEquatable`1。

在确定两个值类型是否相等时,IEquatable<T> 要优于 Equals。通过使用接口,调用方可避免装箱和托管反射的不良性能影响。

确保所有实例数据均设置为零、false 或 null(根据需要)的状态是无效的。

如果遵循这一准则,新构造的值类型实例不会处于不可用的状态。例如,下面的结构的设计是错误的。参数化构造函数有意确保存在有效的状态,但在创建结构数组时不执行该构造函数。这意味着实例字段 label 初始化为 null(在 Visual Basic 中为 Nothing),这对于此结构的 ToString 实现是无效的。

C#

不要显式扩展 System.ValueType。

有些编译器不允许扩展 ValueType

枚举设计

枚举提供成组的常数值,它们有助于使成员成为强类型以及提高代码的可读性。枚举分为简单枚举和标志枚举两种。简单枚举包含的值不用于组合,也不用于按位比较。标志枚举应使用按位 OR 操作进行组合。标志枚举值的组合使用按位 AND 操作检查。

下列指南介绍了枚举设计的最佳做法。

一定要优选使用枚举而不是静态常量。

下面的代码示例演示了不正确的设计。

不要对开放集(如操作系统版本)使用枚举。

向已提供的枚举添加值会中断现有代码。有时可以接受这种做法,但不应在可能出现这种情况的场合设计枚举。

不要定义供将来使用的保留枚举值。

某些情况下,您可能认为为了向提供的枚举添加值,值得冒可能中断现有代码的风险。还可以定义使用其值的新的枚举和成员。

一定不要将 sentinel 值包括在枚举中。

Sentinel 值用于标识枚举中的值的边界。通常,sentinel 值用于范围检查,它不是一个有效的数据值。下面的代码示例定义一个带有 sentinel 值的枚举。

一定要在简单枚举中提供一个零值。

如果可能,将此值命名为 None。如果 None 不适合,请将零值赋给最常用的值(默认值)。

考虑将 System.Int32(大多数编程语言的默认数据类型)用作枚举的基础数据类型,除非出现以下任何一种情况:

  • 枚举是标志枚举,且您有 32 个以上的标志或者期望在将来有更多的标志。
  • 基础类型需要与 Int32 不同,以便易于与期望不同大小的枚举的非托管代码进行互操作。
  • 较小的基础类型可以节省大量空间。如果期望枚举主要用作控制流的参数,其大小就不太重要。如果出现下面的情况,大小节省可能会很重要:
    • 期望枚举被用作非常频繁地实例化的结构或类中的字段。
    • 期望用户创建枚举实例的大型数组或集合。
    • 预计要序列化大量枚举实例。

不要直接扩展 System.Enum。

一些编译器不允许扩展 Enum,除非间接地使用生成枚举的语言特定的关键字来进行扩展。

嵌套类型

嵌套类型是作为某其他类型的成员的类型。 嵌套类型应与其声明类型紧密关联,并且不得用作通用类型。 有些开发人员会将嵌套类型弄混淆,因此嵌套类型不应是公开可见的,除非不得不这样做。 在设计完善的库中,开发人员几乎不需要使用嵌套类型实例化对象或声明变量。

在声明类型使用和创建嵌套类型实例时,嵌套类型很有用,但不在公共成员中公开嵌套类型的使用。

如果嵌套类型和其外部类型之间的关系需要成员可访问性语义,则要使用嵌套类型。

由于嵌套类型被视为是声明类型的成员,因此嵌套类型可以访问声明类型中的所有其他成员。

不要将公共嵌套类型用作逻辑分组构造;请改用命名空间。

避免公开显露嵌套类型。 唯一的特例是需要声明嵌套类型的变量的情况,在生成子类或其他高级自定义等极少数情况下需要声明嵌套类型的变量。

如果可能在声明类型的外部引用类型,则不要使用嵌套类型。

在常见方案中,不应要求对嵌套类型进行变量声明和对象实例化。 例如,处理在某一类上定义的事件的事件处理程序委托不应嵌套在该类中。

如果需要由客户端代码实例化类型,则不要使用嵌套类型。 如果某种类型具有公共构造函数,就可能不应进行嵌套。

理想情况下,嵌套类型仅由它的声明类型进行实例化和使用。 如果嵌套类型具有公共构造函数,则表示该类型不单由其声明类型使用。 通常情况下,嵌套类型不应针对其声明类型以外的类型执行任务。 如某种类型具有更广泛的用途,就很可能不应进行嵌套。

不要将嵌套类型定义为接口的成员。 许多语言不支持这样的构造。

发表评论

电子邮件地址不会被公开。 必填项已用*标注