类型设计规范
从CLR的角度看,只有值类型和引用类型两种类型,但是从框架设计的角度我们把类型从逻辑上分了更多的组。如下所示:
类是引用类型的一般情况,占了框架中的大多情况,类的流行归于它支持面向对象的特征,以及它的普遍的适用性,基类和抽象类是两个特殊的逻辑分组,它们与扩张性有关。
由于CLR不支持多继承,接口类型可以用来模拟多继承,既能被引用类型实现,也能被值类型实现。
结构是值类型的一般情况,应该用于小而简单的类型,就像编程语言的基本类型一样。
枚举是值类型的一个特例,它用来定义一小组值。
静态类是那些用来容纳静态成员的类型,常用来提供对其他操作的快速访问。
委托、异常、Attribute、数据、集合都是引用类型的特例,各有各自的用途。
1.1 类型和名字空间
在设计大型框架之前,应该决定如何将功能划分到一组功能域中,这些功能域由名字空间表示,为了确保一组有条理的名字空间包含的类型能很好的集成,不发生冲突,以及不会重复,自顶向下的设计很有必要。导致了下面的规范:
要用名字空间把类型组织成一个相关的特性域的层次结构。(主要是为了把类型组织成一个有条理、易于浏览的、易于理解的层次结构)
避免非常深的名字空间层次(难于浏览,需要经常回溯)
避免有太多的名字空间。
避免把为高级场景而设计的类型和常见的编程任务而设计的类型放在同一个名字空间中。(方便用户更容易理解框架的基本概念,容易在常见的场景中使用框架)
不要指定名字空间就定义类型。(把相关的类型组织到一个层次结构中,有助于解决可能存在的名字冲突)
标准子名字空间的命名
很少使用的类型应该放在子名字空间中,以免扰乱主名字空间,我们确定了几组类型,应该把它们从主名字空间中区分离出来。
1. .Design子名字空间
仅用于设计时的类型应该放在名为.Design的子名字空间。如:System.Windows.Forms.Design;
System.Messaging.Design;
要用带“.Design”后缀的名字空间来容纳那些为基本名字空间提供设计时的功能的类型。
2. .Permissions子名字空间
许可类型应该放在名为“.Permissions”子名字空间中。
要用带“.Permissions”后缀的名字空间来容纳那些基本名字空间提供自定义许可的类型。
3. .Interop子命名空间
许多框架需要支持与旧系统的互操作性(interoperability)。
要用带“.Interop”后缀的名字空间来容纳那些为基本名字空间提供相互操作功能的类型。
要用带“.Interop”后缀的名字空间来容纳所有位于PIA中的代码。
1.2 类和结构之间的选择
引用类型在堆上分配,由垃圾收集器管理;而值类型要么在栈上分配并在栈展开时释放,要么内联在容纳它的类型中并在容纳它的类型被释放时释放。因此,与引用类型的分配与释放相比,值类型的分配与释放开销更低。
引用类型的数组不是非内联分配的,意为数组元素只是一些引用,指向那些位于堆中的引用类型的实例。而值类型的分配是内联的,数组的元素就是值类型的真正实例。因此值类型的分配和释放的开销要比引用类型的大的多,在大多情况下,值类型数组具有更好的局部性。
值类型在被强制转换为对象或装箱因为装箱和对象是在堆上分配的,且由垃圾收集器管理,所以太多的装拆箱操作会对堆、垃圾收集器,并对系统性能造成影响。
引用类型的赋值是复制引用,而值类型的赋值复制整个值,对大的引用类型复制开销要比值类型小的多。
引用类型是引用传递,值类型是值传递。改变引用类型的一个实例会影响其他的实例,改变值类型的实例,不会影响到它的副本。
框架中的大多数类型应该是类,但是在某些特殊情况下,由于值类型所具有的特征,使用结构更合适。
考虑定义结构而不是类(如果该类型的实例比较小,生命周期比较短,经常被内嵌在其他对象中)
不要定义结构,除非该类型具有以下特征:
它在逻辑上代表一个独立的值,与基本类型相似(int)
它的实例大小小于16字节
它是不可变的
它不需要被经常装箱
在所有的其他情况下,应该将类型定义为类。
1.3 类和接口之间的选择
一般来说,类是用来暴漏抽象的优先选择。
点在于当需要允许API不断演化时,它的灵活性不如类,一旦你发布了一个接口,它的成员就永远固定了,给接口添加任何东西都会破坏已经实现该接口的已有类型。
类提供了更多的灵活性,你可以给一个已发布的类添加成员。只要添加的方法不是抽象的,任何已有的派生类无需改变仍能继续使用。
要优先采用类而不是接口
与基于接口的API相比,基于类的API容易演化得多,因为可以给类型添加成员而不会破坏已有的代码。
要用抽象类而不是接口来解除协定与实现之间的耦合。
抽象类经过正确的设计,同样能够解除协定与实现之间的耦合,与接口能达到的程度不相上下。
要定义接口,如果需要提供一个多态的值类型层次结构的话。(值类型不能自其它类型继承,但是她们可以实现接口)
考虑通过定义接口来达到与多重继承相类似的效果。
1.4 抽象类的设计
不要在抽象类型中定义公有的或内部受保护的构造函数。
只有当用户需要创建一个类型的实例时,该类型的构造参数才是公有的,由于你无法创建一个抽象类的实例,因此如果抽象类型具有公有构造函数
要为抽象类定义受保护的构造函数或内部构造函数。
受保护的构造函数仅仅是允许子类型被创建时,基类能够做自己的初始化。
内部构造函数可以用来把该抽象类的具体实现限制在定义该抽象类的程序集中。
要为发布的抽象类提供至少一个继承自该类的具体类型。
有助于验证该抽象类的设计是否正确。
1.5 静态类的设计
静态类被定义为一个只包含静态成员的类。如果一个类被定义为静态,那么它就是密封的、抽象的,不能覆盖或者声明任何实例成员。
静态类是在纯面向对象设计和简单性之间的一个权衡,它们被广泛用来提供一下访问其他操作的快捷方式,或者不需要完整的面向对象封装器的时候提供一些功能。(System.Enviroment)
要尽量少用静态类
静态类仅被用作辅助类,来支持框架的面向对象的核心。
不要把静态类当做杂物箱。
每一个静态类都应该有其明确的目的。
不要在静态类中声明或覆盖实例成员。
要把静态类定义为密封的、抽象的,并添加一个私有的实例构造函数。
1.6 接口的设计
虽然大多数情况下API用类或结构来构建最好,但是在有些情况下,接口更合适。
CLR 不支持多继承,但允许类型实现一个或多个接口,因此通常用接口来实现多继承。
在创建能够为多种类型(包括值类型)所支持的公共接口时。
要定义接口,如果你需要包括值类型在内的一组类型支持一些公共的API。
考虑定义接口,如果需要让已经自其它类型继承的类型支持该接口提供的功能。
避免使用记号接口(没有成员的接口)
要为接口提供至少一个实现该接口的类型。
要为你定义的每个接口提供至少一个使用该接口的API(一个以接口为参数的方法或是一个类型为该接口的属性)
不要给已发行的接口再添加成员。
这样做会破坏该接口的实现,为了避免版本的问题,应该创建一个新的接口。
一般来说,在为托管代码设计可重用的程序库时,你应该选择类而不是接口。
1.7 结构的设计
通用目的的值类型通常称为struct(结构)。
不要为结构提供默认的构造函数。(C#不允许结构有默认的构造函数)
要确保所有的实例数据都为零,false,或null时,结构仍处于有效状态。(可以防止在创建一个结构时创建出无效的实例)
要为值类型实现IEquatable<T>
值类型的Object.Equals方法会导致装箱,默认的实现并不高效,因为使用了反射,IEquatable<T>.Equals性能好的多,不会导致装箱。
不要显示的扩展System.ValueType
1.8 枚举的设计
枚举是一种特殊的值类型,有两种类型的枚举:简单枚举和标记枚举。
简单枚举代表小型的、闭合的一组选择。例如(一组颜色):
Public enumColor{
Red,
Green,
Blue,
……
}
标记枚举的设计是为了支持对枚举值进行按位操作。标记枚举的常见例子是一个选择列表
[Flags]
Public enumAttributeTargets
{
Assembly=0x0001,
Module=0x0002,
Cass=0x0004,
Struct=0x0008
}
要用枚举来加强那些表示值的集合的参数、属性以及返回值的类型性。
要优先使用枚举而不要使用静态常量。(枚举是一个包含一组静态常量的结构)
不要把枚举用于开发的集合(比如操作系统版本等)
不要提供为了今后使用而保留的枚举值。
避免显示的暴漏只有一个值的枚举。
不要把sentinel值包含在枚举值中。
要为简单枚举类型提供零值。(应该考虑把该值称为None之类的东西,如果这样的值不适合用于某个特定的枚举,那么应该把该枚举中最常用的默认值赋值为0)
考虑用Int32作为枚举的基本实现类型。
要用复数名词或者名词短语来命名标记枚举,用单数名词或者名词短语来命名简单枚举。
不要直接扩充System.Enum
1.8.1 标记枚举的设计
要对标记枚举使用System.FlagsAttribute,不要把该attribute用于简单枚举。
[Flags]
Public enumAttributeTargets
{
…..
}
要用2 的幂次方作为标记枚举的值,这样就可以通过按位或操作自由组合他们。
[Flags]
Public enumWatcherChangeTypes
{
Created=0x0002,
Deleted=0x0004,
Changed=0x0008,
Renamed=0x00010,
}
考虑为常用的标记组合提供特殊的枚举值。(位操作是一个高级概念,对应简单任务来说不是必须的,FileAccess.ReadWrite就是这样一个例子)
[Flags]
Public enumFileAccess
{
Read=1,
Write=2,
ReadWrite=Read|Write,
}
避免让创建的标记枚举包含某些无效的组合。
避免把零用作标记枚举的值,除非该值表示“所有标记都被清除“,而且按下一条规范进行了适当的命名。(CLR规定任何值类型的默认值“所有的位都清零“)
要把标记枚举的零值命名为None,对其标枚举来说,该值必须始终意味着“所有标记均被清除”。
[Flags]
Public enumBorderStyle
{
Fixed3D=0x1,
FixedSingle=0x2,
None=0x0
}
但是,该规则只适用于标记枚举,对于非标记枚举的情况,避免使用0值实际上是不利的,所有的枚举类型一开始都为零值。
1.8.2 给枚举添加值
常会发现在发现之后需要给一个枚举添加值,如果新添加的值是一个已有API的返回值,那么就存在潜在的应用程序兼容性问题。
考虑给枚举添加值,尽管有那么一点兼容性的风险。
如果有实际数据,表明给枚举添加值会导致应用程序的不兼容,可以考虑添加一个新的API来返回新老枚举值,这样就能确保仍然兼容现有的应用程序。
1.9 嵌套类型
嵌套类型是一个定义在另一个类型的作用域内的类型。另一个类型被称为外层类型。嵌套类型能够访问外层类型的所有成员。可以访问定义在外层类型的私有字段以及定义在外层类型的所有父类的受保护字段。
一般来说,尽量少用嵌套类型,嵌套类型与外层类型紧密耦合,不适合将它们作为通用类型。嵌套类型适合用来对它们的外层类型的实现细节建模。
要想让一个类型能够访问外层类型的成员时才使用嵌套类型。
不要用嵌套类型进行逻辑分组,应该用名字空间来达到此目的。
避免公开的暴漏嵌套类型,唯一的例外是如果只需要在极少数的场景中声明嵌套类型的变量,比如派生子类,或者其他高级自定义场景中。
不要使用嵌套类型,如果该类型可能会被除了它的外层类型之外的类型引用。
不要使用嵌套类型,如果它们需要被客户代码实例化。
不要把嵌套类型定义为接口的成员。
一般来说尽量少用嵌套类型,而且应该避免将嵌套类型公开暴漏给外界。