[译]类型设计准则

本文由 CYJB 译自 Type Design Guidelines(.NET Framework 4.5)

对 CLR 来说,只存在两种类型——引用类型和值类型。但是为了讨论框架设计,我们将类型细分为更多的逻辑组,每组有其特定的设计准则。

类是通用的引用类型,框架中的大部分类型都是类。类因其支持面向对象的大部分特性和普遍适应性而大受欢迎。基类和抽象类是与扩展性相关的特殊逻辑组。

接口是可以由引用类型和值类型实现的类型。它们可以作为引用类型和值类型的层次结构的根,或者模拟多重继承(CLR 本身并不支持多重继承)。

结构体是通用的值类型,用于表示小的简单类型,类似于语言的基本类型。

枚举是特殊的值类型,用于定义值的集合,例如星期、控制台颜色等等。

静态类是设计用来包含静态成员的类型,通常用于提供其它操作的快捷方式。

委托、异常、特性、数组和集合都是针对特定用途的特殊引用类型,它们的设计和使用准则会在本书的其它位置讨论。

√ 要确保每个类型都是相关成员的良好定义的集合,而不仅仅是无关函数的随机集合。

选择类或结构体

每个框架设计师都会面对的基本决定之一,就是将一个类型设计为类(引用类型)还是结构体(值类型),因此很有必要了解引用类型和值类型之间的行为区别。

引用类型和值类型的第一个区别,是引用类型分配在堆(heap)上,会被垃圾回收;而值类型分配在栈(stack)上,或被內联入包含类型中,在栈展开或包含类型被释放时回收。所以值类型的分配和回收通常比引用类型的开销更小。

其次,引用类型的数组并不连续分配,也就是说数组元素仅仅是对位于堆上的引用类型实例的引用。值类型的数组则是连续分配的,意味着数组元素实际上就是值类型的实例。所以,值类型数组的分配和回收也比引用类型数组的开销更少。另外,大多数情况下值类型数组比引用类型数组表现出更好的局部性。

然后是与内存使用相关的区别。值类型在被转换为引用类型或实现的接口时会被装箱,在转换回值类型时会被拆箱。由于装箱的结果是分配在堆上的对象,会被垃圾回收,太多的装箱和拆箱会对堆和垃圾回收器造成负面影响,最终会影响到应用的性能。相比之下,引用类型被转换时不会发生装箱。

接下来,引用类型赋值时会复制引用,而值类型赋值时会复制完整的值。所以,巨大的引用类型的赋值,比巨大的值类型的赋值开销更少。

最后,引用类型会按引用传递,而值类型会按值传递。对引用类型实例的改变,会影响到所有指向该实例的引用。值类型的实例会在按值传递时复制,当值类型的实例被改变时,显然不会影响到它的其它副本。由于值类型的副本不是由用户显式创建的,而是在参数传递或返回值返回时隐式创建的,可变的值类型可能会使许多用户迷惑,因此,值类型应当是不可变的。

根据经验,框架中的大部分类型应该是类。然而,还有一些情况,值类型的特性使得其更适合使用结构体。

√ 考虑定义结构体而不是类,如果类型的实例很小,而且通常生命周期短或嵌入在其它对象中。

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

  • 它在逻辑上表示单个值,与基元类型(int、double 等)类似。
  • 它的实例大小小于 16 字节。
  • 它是不可变的。
  • 它不会需要频繁的被装箱。

在其它所有情况下,您都应当把您的类型定义为类。

抽象类设计

X 不要在抽象类中定义 public 或 protected internal 构造函数。

只有用户需要创建类型的实例时才需要公共构造函数。由于您不能创建抽象类型的实例,具有公共构造函数的抽象类型是错误的设计,而且会误导用户。

√ 要为抽象类定义 protected 或 internal 构造函数。

一个 protected 构造函数更加常见,而且允许基类在子类创建时完成它自己的初始化。

一个 internal 构造函数可以用来将抽象类的具体实现限制在定义该类的程序集中。

√ 要为您提供的每个抽象类,提供至少一个具体的继承类型。

这样做有助于验证抽象类的设计。例如,System.IO.FileStream 是 System.IO.Stream 抽象类的一个实现。

静态类设计

一个静态类是只包含静态成员的类(当然除了继承自 System.Object 的实例成员和可能的 private 构造函数)。一些语言提供内建的静态类支持。在 C# 2.0 以及更高版本,当一个类被定义为 static,它就是密封的、抽象的,而且没有可以声明或重写的实例成员。

静态类是在纯面向对象设计和简洁性之间的妥协,它们一般用于提供其它操作的快捷方式(例如 System.IO.File),储存扩展方法或不适合使用完全面向对象包装的功能(例如 System.Environment)。

√ 要谨慎使用静态类。

静态类应当仅用于支持框架中的面向对象核心类。

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

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

√ 要将静态类声明为密封的、抽象的,并且添加一个私有成员构造函数,如果您使用的编程语言没有内建的静态类支持。

接口设计

尽管大部分 API 最适合使用类和结构体建模,有些情况下接口是更合适的或者是唯一的选择。

CLR 并不支持多继承(即 CLR 的类不能继承自多于一个的基类),但它允许类型在继承自一个基类之外实现一个或多个接口。因此,接口经常用于实现多重继承的效果。例如,IDisposable 是一个允许类型支持资源释放的接口,它独立于其它任何继承层次结构。

另一个适合定义接口的情形是创建可以由多个类型(包括值类型)支持的公共接口。值类型不能继承自 ValueType 以外的其它类型,但它们可以实现接口,因此使用接口就成为了能够提供公共基本类型的唯一选项。

√ 要定义接口,如果您需要一些由包含值类型的多个类型支持的公共 API。

√ 考虑定义接口,如果您在已经从其它类型继承的类型上支持其功能。

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

如果您需要将一个类标为具有特殊的特性(标记),一般而言,使用自定义特性(Attribute)而不是接口。

√ 要为接口提供至少一个实现的类型。

这样会有助于验证接口的实现。例如,List<T> 是 IList<T> 接口的一个实现。

√ 要为您定义的每个接口,都提供至少一个用到它的 API(将接口作为参数的方法,或类型为该接口的属性)。

这样会有助于验证接口的设计。例如,List<T>.Sort 会用到 System.Collections.Generic.IComparer<T> 接口。

X 不要向已被公开的接口添加成员。

这样可能会破坏接口的现有实现。您应当创建一个新接口来避免版本问题。

在设计可重用的托管代码库时,除了上面所述的情况,一般您都应当选择使用类而不是接口。

结构体设计

通用值类型常被称作 struct(结构体),这个一个 C# 关键字。这节提供了一般的结构体设计准则。

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

遵循这一准则,允许创建结构体数组而无需为每个数组元素调用构造函数。请注意 C# 并不允许为结构体提供默认构造函数。

X 不要定义可变的结构体。

可变的结构体存在一些问题。例如,当属性的 get 访问器返回了一个值类型,调用者会得到返回值的副本。因为副本是隐式创建的,因此开发者可能并未意识到他们在修改副本,而不是原始值。此外,一些语言(特别是动态语言)在使用可变的值类型时会有问题,因为即使是局部变量,在取消引用时也会产生副本。

√ 要确保所有实例数据被设置为 0false 或者 null(适用时)的状态是有效的。

这是为了防止在创建结构体数组的时候意外的创建了无效的实例。

√ 要在值类型上实现 IEquatable<T>

值类型的 Object.Equals 方法会导致装箱操作,而且它的默认实现由于使用了反射,因而并不非常高效。实现 IEquatable<T>.Equals 方法可以具有更高的性能,而且不会导致装箱。

X 不要显示扩展 ValueType。事实上,大部分语言会阻止这样做。

通常,结构体可以非常有用,但仅应当被用于小的(译注:如上文所述,小于 16 字节)、单一的、不可变的且不会被频繁装箱的值。

枚举设计

枚举是特殊的值类型,分为简单枚举和标志枚举两种。

简单枚举表示了选择的小的闭集。一个常见的简单枚举的例子是一组颜色。

标志枚举是为使枚举值支持位运算而设计的。一个常见的标志枚举的例子是一个选项列表。

√ 要使用枚举强类型化表示值的集合的参数、属性和返回值。

√ 要优先使用枚举而不是静态常量。

X 不要对开放集(如操作系统版本,您朋友的名字等)使用枚举。

X 不要提供计划将来使用的保留枚举值。

您总是可以简单地在后期为现有枚举添加值。请参见向枚举添加值,获取向枚举添加值的更多信息。保留值只会污染了真实的值,并往往导致用户错误。

X 避免公开暴露只包含一个值的枚举。

确保 C API 的未来可扩展性的一个常见做法是为方法签名添加保留参数。这样的保留参数可以被表示含有单个默认值的枚举。在托管 API 中不应该这样做,方法重载允许在未来的版本中添加参数。

X 不要在枚举中包含哨兵值。

尽管哨兵值有时能帮助到框架开发者,但会混淆框架的用户。它们被用于跟踪枚举的状态,而不是枚举表示的集合中的一个值。

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

考虑将零值命名为 "None"。如果 "None" 不适于这个枚举,应当将基础值零分配给枚举最常见的默认值。

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

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

对于在内存中使用枚举,请注意托管对象总是按双字(DWORD)对齐的,因此您实际上最好使用多个枚举或小的结构体填满实例,因为实例的总大小总是会向上舍入到双字(DWORD)。

√ 要以复数名词或名词短语来命名标志枚举,简单枚举则使用单数名词或名词短语。

X 不要直接扩展 System.Enum

System.Enum 是一个特殊类型,由 CLR 用来创建用户定义的枚举。大部分编程语言提供了供您使用这一功能的语言元素。例如,C# 中 enum 关联字就用于定义枚举。

设计标记枚举

√ 要为标记枚举应用 System.FlagsAttribute。不要将这个特性应用到简单枚举上。

√ 要为标志枚举的值使用 2 的幂,以便这些值可以使用按位“或”运算自由组合。

√ 考虑为常用的标志组合提供特殊的枚举值。

按位操作是高级概念,对简单的任务来说不是必须的。ReadWrite 就是这样的特殊值的示例。

X 避免创建标志枚举,当某些组合值无效时。

X 避免使用值为零的标志枚举,除非这样的值表示“所有标志都被清除”,而且按照下一条准则所述的正确命名。

√ 要将标志枚举的零值命名为 “None”。对于标志枚举,该值必须始终表示“所有标志都被清除”。

向枚举添加值

当您已经公开了一个枚举后,也经常会发现您需要向当中添加更多的值。当新加入的值被现有 API 返回时,可能产生程序兼容性问题,因为编写糟糕的应用程序可能无法正确处理这些新值。

√ 考虑向枚举添加值,即使存在小的兼容风险。

如果您确定向枚举添加值会导致程序兼容性问题,那么考虑增加一个返回新值和旧值的新 API,并将旧的 API(仍然只返回旧值)标记为已过时。这样可以确保您的现有程序能够保持兼容。

嵌套类型

嵌套类型是另一种类型(即所谓的封闭类型)的范围内定义的类型,这就是所谓的封闭类型。一个嵌套类型有权访问它的封闭类型的所有成员。例如,它有权访问在封闭类型中定义的私有字段,以及在封闭类型的所有祖先中定义的受保护的字段。

通常应当谨慎使用嵌套类型,有几个方面的原因。一些开发者并不完全熟悉嵌套类型的概念。例如,这些开发人员可能不了解声明嵌套类型变量的语法。而且嵌套类型与它的封闭类型关联非常紧密,因此不适合将其用作通用类型。

嵌套类型最适合于构造它们的封闭类型的实现细节。最终用户应当很少需要声明嵌套类型的变量,并且几乎从不应该需要显式实例化嵌套类型。例如,一个集合的迭代器可以是那个集合的嵌套类型。迭代器通常是由它们的封闭类型实例化的,而且由于很多语言都支持 foreach 语句,迭代器变量很少需要由最终用户来声明。

√ 要使用嵌套类型,当嵌套类型和它的外部类型间的关系需要成员可访问性语义。

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

X 避免公开暴露嵌套类型。唯一的特例是需要声明嵌套类型的变量,例如在子类化或其他高级自定义等罕见情况下。

X 不要使用嵌套类型,如果类型可能会被包含类型的外部引用。

例如,传递给某个类定义的方法的枚举,不应当定义为该类的嵌套类型。

X 不要使用嵌套类型,如果需要由客户端代码实例化类型。如果一个类型具有公共构造函数,它最好不要被嵌套。

如果一个类型可以被实例化,那么一般认为该类型在所属框架中可以独立使用(您可以创建它、使用它和销毁它而无需使用外部类型),因此不应该被嵌套。在与外部类型没有任何关系的情况下,内部类型不应在外部类型的外部广泛重用。

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



Portions © 2005, 2009 Microsoft Corporation. All rights reserved.

Reprinted by permission of Pearson Education, Inc. from Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition by Krzysztof Cwalina and Brad Abrams, published Oct 22, 2008 by Addison-Wesley Professional as part of the Microsoft Windows Development Series.

时间: 2024-10-14 18:26:20

[译]类型设计准则的相关文章

类库开发的设计准则 读书笔记 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 类型与命名空间: 使用命名空间将类型组织到相关功能区域的的层次结构中 避免使用较深的命名空间层次结构 避免使用

类库开发的设计准则 读书笔记 1名称准则

MSDN链接:http://msdn.microsoft.com/zh-cn/library/vstudio/ms229042(v=vs.100).aspx 系列文章列表: 1名称准则:http://www.cnblogs.com/liu-meng/p/4181984.html 2类型设计准则:http://www.cnblogs.com/liu-meng/p/4182737.html 大小写的样式: Pascal 大小写 将标识符的首字母和后面连接的每个单词的首字母都大写. 可以对三字符或更多

《c++编程剖析-问题,方案和设计准则》笔记

1vector的使用 我们只可以使用operator[]和at()去改动那些已经存在于容器中的东西. 而 用reserve()函数不会使得容器中充满函数,需要用resize()函数代替 当不对容器内的元素做任何改动时,记得使用const_iterator 2关于标准成员函数 C++标准库的实现中的成员函数签名并不要求与标准中说明的函数签名一模一样,它可以具有额外的默认函数. 这意味着,不同的标准库的成员函数签名可能不一致. 这也意味着,不存在一个可移植的指向标准库成员函数的指针. 同时也意味着不

数据库设计准则(第一、第二、第三范式说明)

数据库设计准则(第一.第二.第三范式说明) I.关系数据库设计范式介绍 1.1 第一范式(1NF)无重复的列 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性.如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系.在第一范式(1NF)中表的每一行只包含一个实例的信息.简而言之,第一范式就是无重复的列. 说明:在任何一个关系数据库中,第一范式(1NF)是对

流和几条设计准则

时间:2014.05.27 地点:基地 --------------------------------------------------------------------------------------- 一.关于std::cin和std::cout cin和cout的类型是std::istream和std::ostream .这两个类型又分别对应着 std::basic_istream<char> std::basic_ostream<char> 即,是分别是这二者ty

android抽屉导航的设计准则

我阅读了google官方的关于抽屉导航的设计准则,这可以给我带来什么帮助?最起码,我可以知道,抽屉导航适用在什么场景中,使用它时要注意什么事项.App的设计是有规则可以依据的,比如,使用抽屉导航时,是有明确的规则和场景的. The navigation drawer(导航抽屉) is a panel that transitions in from the left edge of the screen and displays the app’s main navigation options

关于委托,事件和类的设计准则

我们必须保持类型设计满足“高内聚,低耦合”,如此才能做到更好的代码重用.将应用拆解成组件类型可以实现高可维护性,并利于编码调试. 按钮点击.鼠标移动.键盘按键通常都是观察者模式的典型应用.封闭的类型对外发布事件,外部用用订阅类型的事件并编码实现在事件触发通知到系统后的操作,想想按钮Button类型来源于Windows.Forms名称空间,通过Dll提供,你没有Button类型的源代码不是一样注册Button的Click事件,从而让Button唤起你的Method,这个Dll就是一个有效封装. 委

【干货分享】Google 的设计准则,素材和资源

在谷歌,他们说, “专注于用户,所有其它的就会水到渠成 ”.他们遵循设计原则,寻求建立让用户惊喜的用户体验.谷歌一直挑战自己,为他们的用户创造一种视觉语言,综合优秀设计的经典原则和创新.谷歌设计规范是一份活的文件,因为它们继续更新最新的设计原则和细节.这是一份值得每位设计师收藏和学习的准则. 您可能感兴趣的相关文章 Web 开发中很实用的10个效果[附源码下载] 精心挑选的优秀jQuery Ajax分页插件和教程 12款经典的白富美型 jQuery 图片轮播插件 让网站动起来!12款优秀的 jQ

记一次类型设计的求索历程

多年以前,在我的刚接触编程语言时,我遇到了一个超出能力范围的类型设计问题.这个问题困扰我多年,让我寝食难安.原因并不是因为这个问题有多复杂,恰恰相反,让我纠结的是,这个问题看起来很简单,而我却找不到一个优秀的解决方法. 俗话说踏破铁鞋无觅处,得来全不费工夫.苦苦求索而不得的多年之后,我从一次系统设计过程中得到了启发,我终于想出了一个激动人心的解决方案.本文除了吹吹牛逼告诉你们我从几年前开始考虑代码的细节外,我还想和你们分享一下我的激动人心的优秀解决方案的产生过程. 问题所在 现在已有一个汽车类,