Skip to content

Latest commit

 

History

History
919 lines (641 loc) · 45.3 KB

C# 框架设计指南.md

File metadata and controls

919 lines (641 loc) · 45.3 KB

C# 框架设计指南

原文链接:https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/

只根据自身使用情况进行了翻译,内容并不完全,仅供参考。

命名风格指南

大小写

变量

  1. 对于所有的公共成员、命名空间、类名和方法名都使用帕斯卡命名法。
  2. 对于局部变量和参数都使用驼峰命名法。

复合词

  1. 复合词当作单个单词使用,不要拆分复合词。

    单词 帕斯卡命名法 驼峰命名法
    Callback Callback callback

大小写敏感

  1. 并非所有语言都是大小写敏感的,在编写类库时避免出现依靠大小写区分的标识符。

常规

单词

  1. 标识符应能够方便的读出来,使用符合阅读习惯的单词顺序。(例如 HorizontalAlignment 的可读性比 AlignmentHorizontal 高得多)
  2. 不要使用下划线和连字符,只能使用字母和数字。
  3. 不要使用匈牙利命名法。
  4. 避免和常用语言的关键字冲突,例如 var, in, select 等。

缩写词

  1. 使用完整的单词而非缩略词。(例如 Windows 和 Win)
  2. 避免使用缩写词。(例如 DNA)

避免使用语言相关的单词

  1. 使用语义明确的单词而非类型名。(例如 GetLength 和 GetInt)
  2. 确实要使用类型名时,尽量用通用形式。(例如 Long 和 Int64)
  3. 当一个标识符没有特殊含义时,使用常用的单词(例如 item, value 等)而非类型名来命名。

为新版本 API 命名时

  1. 使用与旧版本 API 类似的名称以帮助区分。
  2. 尽量通过添加后缀(而非前缀)的方式区分新 API。
  3. 考虑使用一个全新但意义相似的名称。
  4. 无法使用新名称时,尽量使用数字后缀区分新 API。
  5. 不要使用 "Ex" (或类似单词)区分新旧 API。
  6. 为旧 API 添加 64 位版本时,添加一个后缀 "64" 作为新 API 名称。

Assembly 和 DLL

  1. 使用范围较大的单词命名 Assembly 和 DLL。例如 System.Data。

  2. 考虑使用如下格式命名 DLL :

    <Company>.<Component>.dll

    其中 可以含有多个层级,用点隔开。

命名空间

  1. 使用公司/组织/作者名作为命名空间前缀以防止重名。
  2. 使用与版本无关的项目名作为命名空间的第二级前缀。
  3. 不要使用项目组名作为命名空间的标识符(项目组通常不会存在太长时间),考虑使用具体技术栈名称。
  4. 使用帕斯卡命名法,如果你的公司/组织/作者名称不符合一般命名法规则,遵从原有名称。
  5. 推荐在命名空间中使用单词的复数形式。缩写词,商标等除外。
  6. 避免类名/类型名与命名空间重名。

命名空间与类型名冲突

  1. 避免太过简单的类型名,例如 Node, Element, Log 等。用前缀修饰以便更好的区分。

  2. 不同类型的命名空间有不同的命名方式以避免命名冲突:

    • 应用程序模板

      通常,一个项目不会使用多个应用程序模板(例如 System.Windows.FormsSystem.Web.UI)。

      不要在同一个应用程序模板下添加多个同名类。(例如 System.Web.UI 下有一个 Page 类,此时不应该在 System.Web.UI.Adapters 下再添加 Page 类)

    • 基础命名空间

      这些命名空间主要用于开发工具的开发,命名冲突在这类命名空间中不是很常见。

    • 核心命名空间

      主要是 System 下的一系列命名空间,避免使用容易出现命名冲突的标识符。

    • 工具命名空间

      第一前缀和第二前缀相同的一系列命名空间,格式类似于:

      <Company>.<Technology>*

      不要使用会与同一工具下其他命名空间起冲突的名称。

      不要使用会与应用模板命名空间起冲突的名称。

类、结构体与接口

  1. 使用帕斯卡命名法。
  2. 使用名词命名类和结构体,以区别于用动词命名的方法。
  3. 使用形容词命名接口,极少数情况下也可用名词。
  4. 不要给类添加任何前缀(例如"C")。
  5. 考虑给派生类类名加上父类名作为后缀。
  6. 接口名称都以"I"开头。
  7. 成对的接口和类名应该相同,唯一不同的是接口名前有"I"修饰。

泛型中的类型名

  1. 尽量使用描述性的命名而非单个字母来对泛型中的类型名命名。
  2. 如果一定要使用单个字母命名,请使用 "T"。
  3. 所有泛型中的类型名应以 "T" 开头。
  4. 有所约束的类型名应明显的表现出来(例如 ISession 和 TSession)

常用类型的命名

基类/接口 派生/实现命名规则
System.Attribute 派生类添加 "Attribute" 后缀
System.Delegate 派生类添加 "EventHandler"或 "Callback" 后缀,不要直接使用 "Delegate" 作为后缀。
System.EventArgs 派生类添加 "EventArgs"
System.Enum 不要直接从这个类派生,使用关键字 "enum";不要添加 "Enum" 或 "Flag" 后缀。
System.Exception 派生类添加后缀 "Exception"。
IDictionary/IDictionary<TKey, TValue> 实现接口的类添加后缀 "Dictionary"。
IEnumerable, ICollection, IList, IEnumerable<T>, ICollection<T>, IList<T> 实现接口的类添加后缀 "Collection"。
System.IO.Stream 派生类添加后缀 "Stream"。
CodeAccessPermission/IPermission 添加后缀 "Permission"。

枚举类型的命名

除了常规的类型命名规范之外,枚举类型还需遵守以下规则:

  1. 使用单数形式命名,除非枚举类型中的域是字节序。
  2. 对于域是字节序的枚举使用复数形式,例如 Flags。
  3. 不要使用 "Enum" 作为后缀。
  4. 不要使用 "Flag"/"Flags" 作为后缀。
  5. 不要使用枚举中的值作为前缀。

类成员的命名

方法

  1. 使用动词/动词词组给方法命名。

属性

  1. 使用名词/名词词组或形容词命名属性。

  2. 属性名称不应该以 "Get" 开头,考虑使用方法。

  3. 数组类型的属性应以复数形式单词命名,不要使用单数形式单词加上 "List" 或 "Collection" 后缀的形式。

  4. 布尔属性的属性都使用肯定形式,例如,使用 CanSeek 而不是 CantSeek

  5. 考虑使用类型名命名属性,参考下面这个例子:

    public enum Color {...}
    public class Control
    {
      public Color Color { get {...} set {...} }
    }

事件

  1. 用动词/动词词组命名事件。
  2. 根据事件触发的顺序(在某一动作之前/之后),事件应该用动词的进行式/过去式。
  3. 不要使用 Before-/After- 前缀区分先后。
  4. 事件句柄应该加上 "EventHandler" 后缀。
  5. 事件句柄中的参数应该用 sender 和 e。
  6. 事件参数类型应该用 "EventArgs" 作为后缀。

字段

这里的指南适用于 public 和 protected 字段。

  1. 使用帕斯卡命名法。
  2. 使用名词/名词短语或形容词命名字段。
  3. 不要用特殊前缀区分字段。

参数命名

  1. 使用驼峰法命名参数。
  2. 使用具体的单词命名。
  3. 命名时尽量基于参数的意义而非参数的类型。

运算符重载

  1. 二元运算符的参数应使用 "left" 和 "right"。
  2. 一元运算符的参数应使用 "value"。
  3. 如果用有意义的单词命名参数能够显著提高可读性,可以不遵守上述规则。
  4. 不要使用缩写词或数字(例如:parameter1, parameter2 等)命名参数。

资源命名

  1. 使用帕斯卡命名法。
  2. 尽量使用有意义的单词。
  3. 不要使用主流编程语言的关键字。
  4. 只使用字母、数字和下划线命名资源。
  5. 异常信息资源的命名应该使用异常名+标识符的格式命名,标识符是自定义的。

类型设计指南

类还是结构体?

在类(引用类型)和结构体(值类型)之间选择是编程人员经常遇到的选择。正确的理解这两种类型之间的区别是十分关键的。

  1. 引用类型的实例位于堆上,通过垃圾回收机制回收空间;值类型的实例则位于栈上,当生命周期结束后自动从栈中弹出。在这一点上,值类型的空间分配/回收开销要比引用类型小。
  2. 对于数组来说,引用类型的数组实际包含的是一系列对象的引用,而值类型的数组则直接包含这些对象。因此这是值类型数组的空间分配/回收开销反而比引用类型大。
  3. 值类型在转换为引用类型时有一个装箱过程,再转换回值类型时还有一个拆箱过程。大量的装箱/拆箱会影响堆、垃圾回收机制甚至整个程序的性能。
  4. 引用类型只复制引用,而值类型则完全复制内容,当类型占用的空间比较大时,引用类型复制的开销要小于值类型。
  5. 作为参数传递时,引用类型传入的是引用的拷贝,值类型则是值的拷贝。对于引用类型的修改会影响所有该引用类型的复制;对值类型的修改则不会影响其他值类型的复制。因此,作为参数传递时,不应修改值类型的参数。
  6. 当一个类型同时具有空间小、生命周期短和内嵌于其他类中的特点时,考虑使用结构体。
  7. 除非满足以下条件,否则不要使用结构体:
    • 逻辑上代表一个单一的值,类似于 int, double 等。
    • 实例大小小于 16 bytes。
    • 实例是只读的。
    • 不必频繁装箱。

抽象类

  1. 不要为抽象类实现 public 或 protected internal 的构造器。
  2. 抽象类需要实现一个 protected 或 internal 的构造器以方便子类初始化。
  3. 至少为你的抽象类实现一个子类。这有助于对你的抽象类进行测试。

静态类

  1. 谨慎使用静态类,它们只应当作为其他类的支持类。(例如 System.File)
  2. 不要将静态类视为 “其他” 类型,将不好归类的方法全部放在一个静态类中。
  3. 不要在静态类中实现抽象方法。
  4. 如果你的编程语言没有静态类支持,请将静态类声明为 abstract sealed,并添加一个私有的构造器。

接口

  1. 如果 API 需要被多个包含值类型的类实现,使用接口。

  2. 如果一个子类需要用到其他类的功能,考虑使用接口来越过只能有一个父类的限制。

  3. 不要使用标记接口(没有任何成员的接口)。如果你想要对一个类标记,给类名加上前缀/后缀。

  4. 至少提供一个实现了接口的类,这有助于测试接口的有效性。

  5. 至少提供一个使用了接口的 API。例如参数为接口的方法或接口类型的属性。

  6. 不要给已经编写好并发布使用的接口添加成员。这有助于避免使用不同版本的接口带来的兼容性问题。

结构体

  1. 不要为结构体添加默认构造器。
  2. 结构体类型的实例应该是只读的。
  3. 确保结构体中的所有字段/属性的默认值(false, 0, null 等)是有效的。
  4. 为所有结构体实现 IEquatable<T> 接口,这会避免比较时的装箱。
  5. 不要直接从 ValueType 类派生,使用关键字 struct

枚举

枚举主要分为两类,一类是简单枚举,代表一个选项集合;另一类是标志枚举[Flags],可以对选项进行位运算。

  1. 当参数、属性、返回类型是一个封闭数据集中的元素时,使用枚举。

  2. 使用枚举而非静态常量。

  3. 不要用枚举类型表示开放数据集(例如操作系统版本,你朋友的名字等)。

  4. 不要在枚举中预留值,换言之,不要在枚举中建立未使用的值。

  5. 不要建立只有一个值可选的枚举。

  6. 不要在枚举中建立监视哨。

  7. 在枚举中代表 0 的值应该被设置成默认值,常见的有"None"。

  8. 推荐使用 Int32 作为枚举的基础类型,除非满足以下条件:

    • 该枚举是标志类型,且标志数量大于(或未来可能大于) 32 个。
    • 整个项目中的其他代码将枚举视为其他类型的值。
    • 较小的基础类型在以下情况下可能会减少空间开销:
      • 该枚举类型是一个经常被实例化的类/结构体的字段。
      • 代码中用到了该较大的枚举类型的数组。
      • 你计划使用大量该枚举类型的实例。

    注意,对象的空间大小是以双字节为单位的,如果枚举的基础类型对于空间开销的减小不够明显,最终对象的空间开销可能不会发生改变。

  9. 用复数名词命名标志枚举,用单数名词命名简单枚举。

  10. 不要直接从 System.Enum 派生,使用关键字 enum。

标志枚举

  1. 为标识枚举添加 [FlagsAttribute] 标签。
  2. 标识枚举的值应该是 2 的幂,这样就可以对其进行 OR 运算。
  3. 对于常用的标志组合可以单独创建一个值。
  4. 确保标志枚举中的所有组合都是有效的,不要出现无效的组合。
  5. 避免使用 0 值,除非它表示所有标识都清空了。
  6. 标志枚举如果要用到 0 值,必须代表“所有标志位都没有选中”,常见的值为 "None"。

为已有枚举添加值

  1. 尽管可能会带来一些兼容性问题,向已有枚举添加值是允许的。
  2. 如果确实带来了兼容性问题,可以增加一个新的 API,该 API 能根据版本返回旧/新的枚举值,同时将旧的 API 视为“过时“。

嵌套类型

嵌套类型常用于实现包含它的封闭类的细节(例如 Enumerator)。嵌套类型本身不应该被经常使用,并且其使用范围应局限于封闭类中。

  1. 当一个类型需要用到某个类的私有/保护成员时,使用嵌套类型。
  2. 不要使用 public 的嵌套类作为程序逻辑层级的实现,考虑使用命名空间。
  3. 不要将嵌套类型设为 public,除非你需要将嵌套类型的变量需要在外部声明(子类化技术)。
  4. 如果类型需要被外部类使用,不要使用嵌套类型。
  5. 如果类型有公开的构造器,不要使用嵌套类型。
  6. 接口的成员不应使用嵌套类型,很多语言都不支持这一特性。

成员设计指南

成员包括方法、属性、事件、构造器和字段。广义的成员指用户使用框架功能的一切方式。

成员重载

  1. 用描述性的单词命名参数。
  2. 对于相同的参数,不同的重载中的命名应该是相同的。
  3. 如果需要扩展,请将最长的重载设置为虚方法,其余较短的重载应通过最长的重载调用。
  4. 不要在重载成员中使用 ref 和 out 关键字。
  5. 不要出现参数位置相同,类型相似但功能完全不同的重载。
  6. 允许可选参数为 null。
  7. 使用重载而非带默认值的参数。

属性

  1. 对于只读属性,只需要创建 get 即可。注意对于引用类型来说,只有 get 仍然可以修改引用对象。
  2. 不要出现只有 set 的属性,set 的可访问性不应高于 get。如果一个属性只能被设置,考虑使用 Set 开头的方法替代。
  3. 为所有属性提供合适的默认值,确保默认值不会导致漏洞或错误。
  4. 确保所有属性可以被任意顺序设置,如果出现错误,那么异常应在使用值的时候抛出。
  5. 如果 set 抛出了异常,确保属性之前的值已经被保存。
  6. 避免从 get 中抛出异常。

索引属性

  1. 推荐使用索引器提供访问内部数组数据的方法。
  2. 代表数据集合的类型都推荐使用索引器。
  3. 索引属性不应有超过一个参数。如果必须用多个参数,考虑使用方法替代。
  4. 索引器参数只应该使用以下类型:System.Int32, System.Int64, System.String, System.Object 或者枚举。如果必须使用其他类型参数,考虑使用方法替代。
  5. 推荐将索引属性命名为 "Item"。
  6. 如果使用了索引器,不要再提供功能相同的方法。

属性改变事件

  1. 推荐在较高抽象级别的属性改变时触发属性改变事件。
  2. 推荐在属性被外力(非类内方法)改变时触发属性改变事件。

构造器

构造器分为两种:类构造器和对象构造器。

类构造器通常是静态的,不接受任何参数,在该类型被使用之前被自动调用。而对象构造器则会在每个对象被创建时调用,可以接受任意数量的参数。

  1. 构造器应该力求简洁,不应有太多功能。

  2. 如果构造器不能很好的满足设计意图,考虑使用静态工厂方法代替对象构造器。

  3. 利用构造器参数来快速初始化主要的属性。

  4. 如果某个构造器参数只用于设置某个属性,该参数应该与属性同名,用

    this.propty = propty;

    的格式初始化。

  5. 构造器的功能越少越好。

  6. 在必要的情况下可以从对象构造器中抛出异常。

  7. 在类中显式的声明对象构造器,不要使用默认的构造器。

  8. 不要在结构体中显式声明构造器。

  9. 不要在对象构造器中调用虚成员。

类构造器(静态构造器)

  1. 静态构造器应该是私有的。
  2. 静态构造器不应该抛出异常。
  3. 直接给静态字段赋初值而非在静态构造器中赋值,这会加快运行速度。

事件

  1. 触发事件请使用单词 "raise" 而非 "trigger" 或 "fire"。
  2. 使用 System.EventHandler 而非手动建立一个用于建立事件的委托。
  3. 除非你确认事件不用传递任何数据,否则建议使用一个 EventArgs 的子类来作为事件的参数。
  4. 对于非只读类中的非静态事件,请使用 protected virtual 方法触发事件。方法名应该按照 "On" 开头的格式命名。
  5. 触发事件的方法应该只接受一个参数,EventArgs e。
  6. 触发一个非静态事件时,sender 不应为 null。
  7. 触发静态事件时,sender 必须为 null。
  8. 对于前置事件(某个动作发生前触发的事件),确保这个事件触发后用户可以取消。

自定义事件句柄

  1. 事件句柄应该是 void 的。
  2. 第一个参数应为 object sender。
  3. 第二个参数应为 EventArgs e,也可以是 EventArgs 的子类。
  4. 事件句柄不应该有两个以上的参数。

字段

以下规则并不适用于静态常量。

  1. 非静态字段不应该是 public 或 protected 的。
  2. 对不应改变的变量使用 const 修饰。
  3. 对预制对象使用 public static readonly 修饰。
  4. 不要对可变类型使用 readonly 修饰。

扩展方法

定义扩展方法的类(一般称为“支持类”)必须是静态的。

  1. 慎用扩展方法,尤其是当被扩展的类无法改变时。 如果被扩展的类可以更改,考虑直接将方法添加为实例方法。 滥用扩展方法可能会导致 API 混乱,使错误的类也被扩展。
  2. 当出现以下情形时,考虑使用扩展方法。
    • 当需要为所有实现某一接口的类提供公共方法时,考虑使用扩展方法。例如 LINQ 为所有实现了 IEnumerable<T> 的类提供了一组公共方法。
    • 当某个实例方法会破坏依赖关系规则时,考虑实现为扩展方法。例如,利用成员方法 String.ToUri() 获得一个 Uri 对象不符合依赖管理规则(String 必须依赖 Uri),而静态方法 Uri.ToUri(this String str) 则是一种更好的设计。
  3. 避免扩展 System.Object 类。 这会导致 VB 用户无法调用该扩展方法,VB 中对于 Object 类的引用都是在运行时决定的,但扩展方法则是编译时决定的。
  4. 不要将扩展方法与被扩展类型放在同一命名空间中,除非是为了添加接口公共方法或符合依赖管理规则。
  5. 避免在定义签名完全相同的扩展方法,即使它们位于不同的命名空间中。
  6. 当被扩展类型是接口或者扩展方法会在大多数场景下被使用时,考虑将其和被扩展类型放于同一命名空间中。
  7. 不要将扩展方法定义在与其实际功能无关的命名空间中,应该将其定义在与实际功能有关的命名空间中。
  8. 对于扩展方法专用的命名空间,避免使用太过宽泛的命名(例如 Extensions ),应该使用描述性的命名(例如 Routing)。

运算符重载

  1. 避免使用运算符重载。除非重载的类型具有和基本数据类型类似的地位。
  2. 当类型具有和基本数据类型一样地位时,考虑使用运算符重载。 例如(String 类重载了 operator==operator!=
  3. 应为数据类型的结构体提供运算符重载。(例如 System.Decimal
  4. 不要自作聪明的为运算符重载添加太多功能。 例如使用 + 为两个数据库查询做 Union 操作,或者使用 << 写入流。
  5. 不要在被运算的类型之外的地方定义运算符重载。
  6. 当重载某一运算时,应当同时重载其逆运算。 (例如重载了 operator==,那么应当同时重载 operator!=
  7. 在运算符重载的同时,也应当提供具有相同功能的方法。 许多语言仅支持部分或者不支持运算符重载,因此提供对应的方法时很必要的,下表提供了一些常用的命名: |运算符|名称|友好命名| |-------------------------|-------------------|-------------------| |N/A|op_Implicit|To<TypeName>/From<TypeName>| |N/A|op_Explicit|To<TypeName>/From<TypeName>| |+ (binary)|op_Addition|Add| |- (binary)|op_Subtraction|Subtract| |* (binary)|op_Multiply|Multiply| |/|op_Division|Divide| |%|op_Modulus|Mod or Remainder| |^|op_ExclusiveOr|Xor| |& (binary)|op_BitwiseAnd|BitwiseAnd| |||op_BitwiseOr|BitwiseOr| |&&|op_LogicalAnd|And| ||||op_LogicalOr|Or| |=|op_Assign|Assign| |<<|op_LeftShift|LeftShift| |>>|op_RightShift|RightShift| |N/A|op_SignedRightShift|SignedRightShift| |N/A|op_UnsignedRightShift|UnsignedRightShift| |==|op_Equality|Equals| |!=|op_Inequality|Equals| |>|op_GreaterThan|CompareTo| |<|op_LessThan|CompareTo| |>=|op_GreaterThanOrEqual|CompareTo| |<=|op_LessThanOrEqual|CompareTo| |*=|op_MultiplicationAssignment|Multiply| |-=|op_SubtractionAssignment|Subtract| |^=|op_ExclusiveOrAssignment|Xor| |<<=|op_LeftShiftAssignment|LeftShift| |%=|op_ModulusAssignment|Mod| |+=|op_AdditionAssignment|Add| |&=|op_BitwiseAndAssignment|BitwiseAnd| ||=|op_BitwiseOrAssignment|BitwiseOr| |,|op_Comma|Comma| |/=|op_DivisionAssignment|Divide| |--|op_Decrement|Decrement| |++|op_Increment|Increment| |- (unary)|op_UnaryNegation|Negate| |+ (unary)|op_UnaryPlus|Plus| |~|op_OnesComplement|OnesComplement|

重载==运算符

重载 == 运算符十分复杂,它的功能需要兼容一些其他的方法,例如 Object.Equals

重载类型转换运算符

  1. 当某种转换并非必要时,不要提供类型转换。
  2. 不要为领域之外的类型提供类型转换,使用构造器完成类似功能。 例如 Int32DoubleDecimal 都是数字类型,但 DateTime 不属于这一领域,因此不应该定义从数字类型到日期类型的类型转换,而应该使用构造器来转换。
  3. 如果某一转换可能会损失数据(丢失精度),不要为其提供隐式转换。
  4. 不要在隐式转换中抛出异常,因为转换发生的位置通常很难确定。
  5. 当转换会造成数据损失且不可接受时,应抛出 System.InvaildCastException 异常。

参数

  1. 在允许的情况下,应尽量使用派生级别最低的类型作为参数类型。
  2. 不要预留参数,当需要更多参数时,利用重载实现一个新版本的成员。
  3. 不要在公共方法中用指针、指针数组或者多维数组作为参数,这些类型通常很难使用。
  4. 将所有 out 参数放于传值参数和传引用(ref)参数之后,即使这会导致和其他重载方法参数排列不一致。
  5. 当重载方法或者实现接口时,应该保持参数名称一致。

布尔类型还是枚举类型?

  1. 当一个成员含有两个及以上的布尔类型参数时,应当使用枚举类型代替。
  2. 除非完全确定不会出现两种以上的情形,否则不要使用布尔类型。
  3. 如果参数仅用于在构造器中对布尔类型进行初始化,考虑使用布尔类型。

参数校验

  1. 应当对 publicprotected 或显式实现的成员中的参数进行校验。当校验失败时抛出 System.ArgumentException (或其子类)异常。 注意,校验过程不一定要在 publicprotected或显式实现的成员中直接完成,也可以通过 privateinternal 成员进行实际的校验过程。
  2. 当参数为 null 且不可接受时,应当抛出 ArgumentNullException 异常。
  3. 应当对枚举类型的参数进行校验。 由于任意整型数据(int)可以被转换为枚举,因此不应该默认枚举类型是一个有效的参数。
  4. 不要使用 Enum.IsDefined 对枚举类型进行校验。
  5. 应当注意到可变类型在校验之后值可能会发生改变,先复制一份传递进来的参数再做处理可以避免这种状况。

参数传递

对于框架设计人员来说,参数传递主要有三种方式:传值、传引用(ref)和输出类型(out)。

当参数通过传值方式传递时,需要参数的成员实际获得的是传递进来参数的一份拷贝。如果参数是值类型的,参数值的一份拷贝会被压在栈上;如果参数是引用类型的,引用的一份拷贝会被压在栈上。

当参数通过传引用方式传递时,需要参数的成员实际获得的是指向传递进来参数的引用。如果参数是值类型的,一份指向该值类型的引用会被压在栈上;如果参数是引用类型,一份引用的引用会被压在栈上。传引用方式允许被调用方修改调用方传入的参数。

输出类型和传引用十分类似,区别在于参数默认是未初始化的,被调用方需要先对其赋值才可以使用。同时,在被调用方返回之前,该参数必须被赋值。

  1. 避免使用 outref 参数。 使用outref 要求使用者具有指针相关的知识,且熟悉引用类型和值类型之间的区别。于此同时,了解 outref 之间的区别对大部分使用者来说有些困难。 如果你的框架针对的是普通开发者,那么你不应该默认使用者了解以上信息。
  2. 不要通过传引用的方式传递引用类型。 当然也有一些例外,例如用于交换两个引用类型的方法。

不定参数

当参数个数不固定时,可以使用params关键字指定不定参数,这会为输入的参数创建一个临时数组。

  1. 当需要用户传入只含有几个元素的数组时,考虑使用不定参数。
  2. 当所需参数在大部分情况下已经被包含在一个数组中时,避免使用不定参数。
  3. 当需要修改参数的内容时,不要使用不定参数。 许多编译器在传入不定参数时会将参数复制到一个临时数组中,因此对于这个数组的所有更改都会丢失。
  4. 考虑为较为简单的重载添加不定参数,即使这会导致重载之间参数不一致的问题。
  5. 为使用不定参数,重新排列参数顺序是可以接受的。
  6. 在注重性能的场合,考虑用多个参数数目不同的重载来代替不定参数,这可以避免频繁的创建临时数组。
  7. 注意不定参数可以为 null,应该对这种情况进行校验。
  8. 不要使用可变参数(即 ...),这不符合 CLS 规范。

指针作为参数

通常情况下,指针不应该公开在框架外。

  1. 尽可能的用其他方式替代指针参数,指针并不符合 CLS 规范。
  2. 当校验会造成较大的性能开销时,避免对指针参数做校验。
  3. 使用指针时应该遵守指针相关的约束。 例如不需要传入开始位置,简单的算术运算即可获得开始位置。

扩展性设计指南

未密封类(可继承类)

  1. 考虑提供可继承的类,这些类里不包含虚成员或受保护成员。这样可以显著提高框架的扩展性。

受保护的成员

  1. 对于高度定制的场景,考虑使用受保护的成员。
  2. 应当将受保护的成员视为公开成员并为其提供文档和兼容性分析。

事件与回调

  1. 推荐为使用者提供回调函数以便使用者增加自定义代码。
  2. 由于事件不需要用户掌握面向对象的程序设计风格,推荐使用事件来允许用户改变框架的默认行为。
  3. 应当优先使用事件而非简单回调,这对开发者更为友好。
  4. 在注重性能的场合下,避免使用回调函数。
  5. 应当使用 Func<>Action<>Expression<> 而非自定义委托来定义回调。
  6. 应当关注到 Expression<>Action<>Expression<> 之间的区别。 它们绝大多数情况下是相同的,但 Expression<> 代表它实际上是在其他进程或远程执行的,另外两个则更倾向于在本地执行。
  7. 应当注意到调用委托时其中的代码是任意的,这可能会造成安全、正确性和兼容性问题。

虚成员

虚成员比起事件与回调有更高的设计、文档和测试开销。

  1. 除非有足够的理由,否则不要使用虚成员。 虚成员在修改时很难保持兼容性,因此应该三思而后行。
  2. 推荐仅对于必要的情况提供扩展性支持。
  3. 虚成员应该被声明为 protected 而非 public 的。 公开方法可以通过调用虚成员来获得扩展性。

抽象(抽象类与接口)

  1. 除非提供了合适的实现,否则不要提供抽象类与接口。
  2. 应当谨慎的比较抽象类与接口,选择一种合适的。
  3. 推荐为抽象类与接口提供对应的测试用例。

抽象基类

  1. 考虑将用于继承的基类声明为 abstract,即使其中不包含任何抽象成员。
  2. 考虑将用于继承的基类放置在其他命名空间中,通常使用者不关心抽象基类的具体实现。
  3. 如果某一个类是用于公开 API 的,避免使用 Base 词缀。

密封类

  1. 不要轻易的将一个类声明为sealed,这会降低扩展性。 在以下情境中,考虑将类声明为 sealed
    • 静态类
    • 类的受保护成员保存了重要数据,不应该被其子类访问。
    • 类继承了大量虚成员。
    • 类携带的属性(Attributes)需要被快速查询。
  2. 不要在密封类型中声明受保护成员或者虚成员。
  3. 考虑将重载的成员声明为 sealed

异常设计指南

抛出异常

  1. 不要返回错误代码。在框架中应该使用异常来处理错误。
  2. 当遇到执行错误(无法完成其功能)时应抛出异常。
  3. 在遇到错误时应立即终止执行的场合,考虑使用System.Environment.FailFast而非抛出异常。
  4. 不要在正常程序流中使用异常。 在用户正确使用的情况下,框架不应当抛出异常。例如用户可以通过调用条件检查方法来避免实际执行的方法抛出异常。
  5. 考虑抛出异常所带来的性能损失。(100 个/秒的速度就会带来明显的性能下降)。
  6. 为所有公开成员所抛出的异常书写文档,并在版本更新时保持一致。
  7. 不要为公开成员提供是否抛出异常的选项。
  8. 不要把异常作为返回值返回。
  9. 推荐使用工厂方法新建异常。
  10. 不要在异常过滤块中抛出异常。 catch(Exception e) when (Fliter(e))
  11. 不要显式的在 finally 块中抛出异常。

标准异常类型

ExceptionSystemException

  1. 不要抛出 System.ExceptionSystem.SystemException
  2. 不要在你的框架中捕获 System.ExceptionSystem.SystemException
  3. 避免捕获System.ExceptionSystem.SystemException,除了抽象层次最高的异常处理块。

ApplicationException

  1. 不要抛出 ApplicationException 及其派生类。

InvalidOperationException

  1. 当对象处于不正确的状态时,抛出 InvalidOperationException

ArgumentExceptionArgumentNullExceptionArgumentOutOfRangeException

  1. 当参数不正确时,抛出 ArgumentException 或其合适的子类。
  2. 抛出ArgumentException 或其子类时应指定 ParamName 属性。
  3. 属性的 set 访问器中应该使用 value 作为参数名称。

NullReferenceExceptionIndexOutOfRangeExceptionAccessViolationException

  1. 不要让公开 API 抛出上述异常,这些异常通常都是由运行时抛出的。

StackOverflowException

  1. 不要显式的抛出 StackOverflowException,这个异常通常由运行时抛出。
  2. 不要捕获 StackOverflowException,正确的处理该异常几乎是不可能的。

OutOfMemoryException

  1. 不要显式抛出 OutOfMemoryException,这个异常通常由运行时抛出。

ComExceptionSEHExceptionExecutionEngineException

  1. 不要显式抛出这些异常,它们通常由运行时抛出。

异常与性能

  1. 不要因为性能原因而使用返回错误码的方式。

测试-执行模型

在实际执行操作之前先检查一些条件以避免异常的发生,例如:

ICollection<int> numbers = ...   
...  
if(!numbers.IsReadOnly){  
    numbers.Add(1);  
}
  1. 考虑使用测试-执行模型来避免频繁抛出异常带来的性能损失。

Try-Parse 模型

在十分注重性能的场合,可以使用 TryParse 提供一个不会抛出异常的版本。

public struct DateTime {  
    public static DateTime Parse(string dateTime){   
        ...   
    }  
    public static bool TryParse(string dateTime, out DateTime result){  
        ...  
    }  
}
  1. 推荐使用 Try-Parse 模型来避免频繁抛出异常带来的性能损失。
  2. 依照这类模型的方法应该返回布尔类型,方法名带有 “Try” 前缀。
  3. 应该同时提供一个功能相同但会抛出异常的方法。

内置类型使用指南

数组

  1. 在公共 API 中应该优先使用集合而非数组。
  2. 不要为数组添加 readonly ,只读数组中的元素仍然可以改变。
  3. 推荐使用交错数组而非多维数组,交错数组的空间利用率更好,且运行时为交错数组的访问做了优化。

特性

  1. 应当为自定义特性类添加词缀 Attribute
  2. 应当为自定义特性添加 AttributeUsageAttribute
  3. 为可选参数提供 set 访问器。
  4. 为必需参数提供只读访问器。
  5. 在构造器中为必需参数提供同名参数以供初始化。
  6. 避免在构造器中为可选参数提供同名参数。
  7. 避免重载自定义特性的构造器。
  8. 在允许的条件下为自定义特性类添加 sealed 关键字。

集合

  1. 不要在公开 API 中使用类型抽象程度较低的集合(例如基类型的集合)。
  2. 不要在公开 API 中使用 ArrayListList<T>
  3. 不要再公开 API 中使用 HashTableDictionary<TKey, TValue>,考虑使用IDictionary 代替。
  4. 不要使用 IEnumerator<T>IEnumertor 或者任何实现了该接口的类型,除了 GetEnumerator 方法的返回值。
  5. 不要再一个类型中同时实现 IEnumerator<T>IEnumerable<T> 接口,这也同时适用于这两个接口的非泛型版本。

集合作为参数

  1. 使用允许范围内抽象程度最低的类型,一般使用 IEnumerable<T>
  2. 避免仅为了使用 Count 属性而使用 ICollection<T>ICollection 作为参数类型。

集合作为属性和返回值

  1. 不要为集合属性提供 set 访问器。
  2. 使用Collection<T>或其子类作为可读可写的属性或返回值。
  3. 使用 ReadOnlyCollection<T>或其子类作为只读的属性或返回值。
  4. 推荐使用泛型 Collection 的子类而非直接使用 Collection。
  5. 对于十分常用的方法或属性,考虑返回Collection<T>ReadOnlyCollection<T>的子类。 这样可以方便以后向集合类型添加支持方法或者修改实现。
  6. 对于由唯一主键的内容,考虑使用带主键的集合。 本条不适用于注重内存开销的场合。
  7. 集合作为属性或者返回值时,方法或属性不应返回 null,用一个空集合代替它。

快照还是实时状态?

代表某一时刻状态的集合称为快照。例如数据库查询返回的集合。另一种集合则代表了实时状态,例如ComboBox的集合。

  1. 属性不应该返回快照,属性应该代表实时状态。
  2. 对于 volatile 的集合,要么使用快照集合,要么使用实时状态的集合。

数组还是集合?

  1. 应当优先使用集合。 集合的功能较数组而言更为强大。 对于只读情景而言,频繁的复制数组会带来明显的性能开销。 但当开发底层 API 时,内存开销较小且访问更快的数组是一个更好的选择。
  2. 在底层 API 中使用数组来减少内存开销并提高性能。
  3. 使用 byte 数组而非集合。
  4. 如果每次调用属性都会返回一个拷贝,不要使用数组。

实现自定义集合

  1. 考虑继承 Collection<T>ReadOnlyCollection<T>或者KeyedCollection<TKey, TItem>来实现新的集合类型。
  2. 为新的集合类型实现 IEnumerable<T> 接口。在合理情形下实现 ICollection<T>IList<T>
  3. 如果使用集合的 API 需要,推荐同时实现非泛型接口(例如 IListICollection)。
  4. 避免在包含大量非集合 API 的类型中实现集合接口。
  5. 不要继承非泛型的基础集合类型(例如 CollectionBase)。

自定义集合的命名

  1. 为实现了 IDictionary 或者 IDictionary<TKey, TValue> 的类添加 “Dictionary” 词缀。
  2. 为实现了 IEnumerable 的类添加词缀 Collection
  3. 为自定义数据结构添加合适的名称。
  4. 避免使用具体的实现作为词缀,例如 LinkedList 或者 Hashtable
  5. 推荐用元素的类型最为集合类型的前缀。
  6. 推荐为只读的集合添加 ReadOnly 前缀。

序列化

.NET Framework 提供了三种序列化方式。

序列化方式 主要类型 使用场景
数据合约序列化 DataContractAttribute
DataMemberAttribute
DataContractSerializer
NetDataContractSerializer
DataContractJsonSerializer
ISerializable
一般序列化
网络服务
JSON
XML 序列化 XmlSerializer XML 序列化
运行时序列化 SerializableAttribute
ISerializable
BinaryFormatter
SoapFormatter
.NET Remoting
  1. 在设计一种新类型时应该考虑如何序列化的问题。

选择合适的序列化技术

  1. 如果你的类型需要被持久化保存或者被网络服务使用,考虑支持数据合约序列化方式。
  2. 如果你的类型需要通过控制 XML 格式来传送信息(例如 XML 标签),考虑用 XML 序列化代替或追加给数据合约序列化技术上。
  3. 如果你的类型实例需要通过 .NET Remoting 处理,考虑支持运行时序列化。
  4. 避免只为了持久化保存而使用 XML 序列化,优先使用数据合约序列化方式。

数据合约序列化

通过为你的类型增加 DataContractAttribute 以及为其成员添加 DataMemeberAttribute 来支持数据合约序列化。

  1. 如果某个数据成员需要在部分信任的情况下被序列化,考虑将其设置为公开的。
  2. 具有DataMemberAttribute的属性都应当实现 get 和 set 访问器。
  3. 推荐使用序列化回调函数来实现反序列化后的实例初始化,因为不能调用反序列化后对象的构造器。
  4. 当对象的内容十分复杂时,推荐使用KnownTypeAttribute来指定反序列化时需要使用的数据类型。
  5. 当增加或者修改序列化方式时,务必注意前后兼容性。 保证修改后的能够被反序列化为修改前的类型,反之亦然。
  6. 实现 IExtensionDataObject接口来允许不同版本的类型互相转换。 IExtensionDataObject.ExtensionData属性可以保存新版本中存在而旧版本中没有的成员,这样从新版本转换到旧版本时数据不会损失。而当从旧版本再转回新版本时,这些数据仍然可以取回。

XML 序列化

  1. 避免专门 XML 序列化设计你的类型,除非你有对 XML 结构控制的强烈需求。XML 序列化技术已经被数据合约序列化所代替。
  2. 如果你需要更为细致的控制 XML 结构,考虑实现 IXmlSerizable接口以获得这些功能。

运行时序列化

  1. 如果你的类型需要被 .NET Remoting 服务使用,考虑实现运行时序列化。

  2. 如果你需要对序列化过程的完全控制,考虑使用运行时序列化。

  3. 应当提供和下列模板完全一致的序列化构造器:

    [Serializable]  
    public class Person : ISerializable {  
        protected Person(SerializationInfo info, StreamingContext context) {  
            ...  
        }  
    }
  4. 应当显式的实现 ISerializable 接口。

  5. 应当为 ISerializable.GetObjectData 实现链接需求。这保证了只有完全受信任的代码以及运行时序列化器才能访问这些成员。

System.Xml

  1. 不要使用 XmlNode 或者 XmlDocument 来代表 Xml 数据。应当使用 IXPathNavigableXmlReaderXmlWriter 或者 XNode 的子类。XmlNodeXmlDocument 不是为了公开 API 所设计的。
  2. 应当使用 IXPathNavigableXmlReader 或者 XNode 的子类作为输入和返回 XML 数据时的数据类型。
  3. 如果你需要设计一个底层的 XML 数据类,不要从 XmlDocument 类继承。

相等关系运算符

相等关系运算符包含 operator==operator!=

  1. 不要只重载一个相等运算符。
  2. 应当保证 Object.Equals 方法和相等运算符有相同的功能和性能开销。 绝大多数情况下这意味着,一旦重载了相等运算符,Object.Equals也应该被重载。
  3. 相等运算符应该避免抛出异常。

值类型的相等关系运算符

  1. 如果相等关系是有意义的,应当重载值类型的相等关系运算符。

引用类型的相等关系运算符

  1. 如果引用类型是可变的,避免重载相等关系运算符。 许多语言对于引用类型有默认的相等关系运算符实现(即判断是否为同一个引用),重载可变类型的相等关系运算符之后会给开发者带来误解,因为引用比较变为了值比较。 不可变则一般没有这个问题,值和引用的相等关系一般是一致的。
  2. 如果判断相等关系会带来明显的性能开销,避免重载相等关系元算符。

常见设计模式

依赖属性

  1. 如果你需要支持 WPF 特性(例如样式、触发器、数据绑定、动画等),提供依赖属性。

设计依赖属性

  1. 实现依赖属性时,应当从 DependencyObject或其子类继承。
  2. 为所有依赖属性提供一个普通的 CLR 属性以及公开静态的System.Windows.DependencyProperty字段。
  3. 应当通过调用 DependencyObject.GetValueDependencyObject.SetValue实现依赖属性。
  4. 应当为静态依赖属性字段添加前缀 Property
  5. 不要显式的在代码里声明依赖属性的默认属性,应当再元数据中指定。
  6. 不要在属性的访问器中添加除了访问静态字段以外的代码。
  7. 不要在依赖属性中保存需要安全性的数据,私有的依赖属性仍然可以被公开的访问。

依赖属性的验证

  1. 不要在依赖属性的访问器中添加验证代码。通过向 DependencyProperty.Register方法转入一个验证回调来指定验证方法。

依赖属性变更提示

  1. 不要在依赖属性的访问器中实现属性变更提示,在属性元数据中添加变更提示回调方法来实现这一功能。

依赖属性的强制赋值

如果传入依赖属性 set 访问器的值在实际赋值给属性之前就发生了改变,我们称其为强制赋值。

  1. 不要在依赖属性的访问器中实现强制赋值逻辑。在属性元数据中保存一个强制赋值回调来实现它。

释放模式

运行时可以自动管理内存,但有很多其他资源需要手动释放,在这种情形下,开发者需要实现IDisposable以及重写 Finalize方法。

  1. 如果