zoukankan      html  css  js  c++  java
  • [译]类型设计准则

    本文由 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.

  • 相关阅读:
    deeplearning.ai 卷积神经网络 Week 1 卷积神经网络
    deeplearning.ai 构建机器学习项目 Week 2 机器学习策略 II
    deeplearning.ai 构建机器学习项目 Week 1 机器学习策略 I
    deeplearning.ai 改善深层神经网络 week3 超参数调试、Batch Normalization和程序框架
    deeplearning.ai 改善深层神经网络 week2 优化算法
    deeplearning.ai 改善深层神经网络 week1 深度学习的实用层面
    cs231n spring 2017 lecture8 Deep Learning Networks
    cs231n spring 2017 lecture7 Training Neural Networks II
    cs231n spring 2017 lecture6 Training Neural Networks I
    cs231n spring 2017 Python/Numpy基础
  • 原文地址:https://www.cnblogs.com/cyjb/p/TypeDesignGuidelines.html
Copyright © 2011-2022 走看看