类是引用类型的一般情况,它占了框架中的大多数类型。类的流行要归功于它支持面向对象的特性,以及它的普遍适用性。积累和抽象类是两个特殊的逻辑分组,他们与扩展性有关。
接口类型既可以被引用类型实现,也能被值类型实现。这使它能够作为由引用类型和值类型组成的多态层次的根。此外,CLR本身不支持多重集成,可以用接口来模拟多重继承。
结构是值类型的一般情况,应该用于小而简单的类型,就像编程语言的基本类型一样。
枚举是值类型的一个特例,它用来定义一小组值,比如颜色,星期几。
静态类是那些用来容纳静态成员的类型,他们常用来提供对其他操作的快捷访问。
委托、异常、attribute、数组以及集合都是引用类型的特例。
要确保每个类型由一组定义明确、相互关联的成员组成,而不仅仅是一些无关功能的随机集合。
有一条很好的建议,如果事情变得太复杂了,那么就定义更多的类型。
一、类与命名空间
要用命名空间把类型组织成一个相关的特性域或层次结构,而不是仅仅用命名空间来区分类型之间的冲突
避免用非常深的命名空间层次,这样的层次难于浏览。
避免有太多的名字空间
在常见场景中,框架的用户应该不需要导入许多的命名空间,如果可能,就应该把常见场景中一起使用的类型放在一个单独的命名空间中。
避免把为高级场景而设计的类型恶化为常见编程任务而设计的类型放在同一个命名空间中,这会使得用户不仅能够容易理解框架的基本概念,而且能够更容易的在常见的场景中使用框架。
有一个不错的建议是,将高级的类型放在简单类型的命名空间的自命名空间中,比如
简单命名空间为 System.Main
高级类型命名空间为 System.Main.Advanced
很少使用的类型应该放在子命名空间中,以免扰乱主命名空间。
二、类与结构之间的选择
引用类型在堆上分配,由垃圾收集器管理
值类型要么在栈上分配并且在栈展开时释放,要么内联在容纳它的类型中并在容纳它的类型被释放时候释放。因此与引用类型的分配和释放相比,值类型的分配与释放开销更低。
值类型在被强制转换成对象或他们实现的接口之一时装箱,在被强制转换回值类型时拆箱。
因为箱子和对象是在堆上分配的,而且由垃圾收集器管理,所以太多的装箱和拆箱操作会对堆、垃圾收集器、并最终对应用程序性能造成负面影响。
引用类型的赋值是复制引用,而值类型的赋值是复制整个值,因此对打的引用类型的赋值的开销要比对大的值类型的赋值的开销要低
最后,引用类型是以引用方式传递的,而值类型是以值方式传递的。改变引用类型的一个实例会影响到所有指向该实例的引用。
值类型的实例在以值方式传递时被复制。当值类型的任何一个实例被改变时,显然不会影响到它的任何副本。由于副本不是用户显示创建的,而是在传递参数或返回值时隐式创建的,因此可以改变的值类型会把许多用户搞糊涂。因此值类型应该是不可变的。
作为一条经验法则,框架中的大多数类型应该是类。但是在有些情况下,由于值类型所具备的特征,使用结构会更合适。
考虑定义结构而不要定义类,如果该类型的实例比较小而且生命周期比较短,或者常常被内嵌在其他对象中。
三、类和接口之间的选择
一般来说,类是用来暴露抽象的优先选择。
接口的主要缺点在于,当需要允许API不断演化时,他的灵活性不如类。一旦你发布了一个接口,它的成员就永远固定了。给接口添加任何东西都会破坏那些实现了该接口的已有类型。
演化基于接口的API唯一方法就是添加一个额外成员的新接口
要优先采用类而不是接口
与基于接口的API相比,基于类的API容易演化得多,因为可以给类添加成员而不会破坏已有的代码。
一个很好的建议就是,可以定义一个接口,然后定义一个实现该接口的抽象基类。让使用者来选择是从接口派生还是从抽象基类派生
要用抽象类而不是用接口来解除协定与实现之间的耦合
要定义接口,如果需要定义一个多态的值类型层级结构。因为值类型不能自其他类型继承。
一个精心定义的接口的另一个标志是一个接口只做一件事情。
四、抽象类设计
不要在抽象类型中定义公有的或者内部受保护的构造函数
因为抽象类无法创建实例,所以把构造函数定义成共有或受保护不仅不当,而且还会误导用户。
要为抽象类定义受保护的构造函数或内部构造函数。
受保护的构造函数仅仅是允许当子类型被创建时,才能初始化。
内部构造函数可以用来把该抽象类的具体实现限制在定义该抽象类的程序集中。
要为发布的抽象类提供至少一个集成自该类的具体类型,这样有助于检验该抽象类的设计是否正确。
五、静态类设计
如果一个类被定义成静态类,那么它就是密封的,抽象的,不能覆盖或声明任何实例成员
要尽量少用静态类
静态类应该仅仅被用作辅助类,来支持框架的面向对象的核心。
不要把静态类当作杂物箱
每一个静态类都应该有明确的目的
不要在静态类中声明或覆盖实例成员
六、接口设计
要为接口提供至少一个实现该接口的类型,这样有助于检验接口的设计
要为你定义的每个接口提供至少一个使用该接口的API(一个以该接口为参数的方法,或者一个类型为该接口的属性),这样有助于检验接口的设计
不要给已经发行的接口再添加成员,这样会破坏该接口的实现,为了避免版本问题,应该创建新的接口。
七、结构设计
不要为结构提供默认的构造函数
这样就允许在创建结构的数组时不必运行数组中每一项的构造函数
要确保当所有的实例数据都为零、false或null时,结构仍然处于有效状态
要为值类型实现IEquatable<T>
值类型的Object.Equals方法会导致装箱,而且它的默认实现也不是非常高效,因为它使用了反射。Iequatable.Equals的性能要好很多,而且能够实现为不会导致装箱
八、枚举的设计
枚举是一种特殊的值类型。有两种类型的枚举:简单枚举和标记枚举
简单枚举代表小型的、闭合的一组选择。简单枚举的一个常见例子就是一组颜色,例如:
Public enum Color
{
Red.
Green,
Blue
}
标记枚举的设计是为了支持对枚举值进行按位操作。标记枚举的一个常见的例子是一个选择列表,例如:
[Flag]
Public enum AttributeTargets
{
Assembly = 0x0001,
Module = 0x0002
}
要用枚举来加强那么表示值的集合的参数,属性以及返回值的类型。
要优先使用枚举而不是使用静态常量
枚举是一个包含一组静态常量的结构,如果你定义一个枚举而不是手工定义一个包含静态常量的结构的话,那么你就会得到一些额外的编译器和反射支持。
不要提供为了今后使用而保留的枚举值
即使是在后期,你页可以能够给已有的枚举添加至。保留值只会污染实际值的集合,还往往会把用户引向错误。
避免显示地暴露只有一个值的枚举
要为简单枚举提供零值
可以考虑把该值称为“None”之类的东西。如果这样的值不适合用于某个特定的枚举,那么应该把该枚举中最常用的默认值赋值为零
Public enum Copression
{
None = 0,
Gzip,
Deflate
}
Public enum EventType
{
Error = 0,
Warning,
Information
}
给枚举添加值
给枚举添加值很可能破坏已有的客户端代码。因为你并不知道客户端代码是使用SWITCH语句对枚举进行穷举还是在更大的范围内对枚举值做渐进分析。
在开发的时候为枚举的后续版本预先做好准备是必须的态度。