第3章 类型设计规范
ü?要确保每个类型由一组定义明确、相互关联的成员组成。
ü?要用命名空间把类型组织成一个相关的特性域的层次结构。
?
??避免非常深的名字空间层次。这样的层次难于浏览,因为用户不得不经常地回溯。
?
??避免有太多的名字空间。
?
??避免把为高级场景而设计的类型和为常见编程任务而设计的类型放在同一个名字空间中。
?
??不要不指定名字空间就定义类型。
ü?要深入了解引用类型和值类型在行为上的差异。
?
ü?作为一条经验法则,框架中的大多数类型应该是类。但是在有些情况下,由于值类型所具备的特征,使用结构会更为合适。
?
ü如果该类型的实例比较小而且生命期比较短,或者经常被内嵌在其他对象中,考虑定义结构而不要定义类。
?
??不要定义结构,除非该类型具有以下所有特征:
??它在逻辑上代表一个独立的值,与基本类型(int、double等等)相似。
??它的实例的大小小于16个字节。
??它是不可变的。
??它不需要经常被装箱。
在所有其他情况下,应该将类型定义为类。
ü?要优先采用类而不是接口。
说明:与基于接口的API相比,基于类的API容易演化得多,因为可以给类添加成员而不会破坏已有的代码。
?
ü?要用抽象类而不是用接口来解除协定与实现之间的耦合。
说明:抽象类经过正确的设计,同样能够解除协定与实现之间的耦合,与接口所能达到的程度不相上下。
?
ü?如果需要提供一个多态的值类型层次结构的话,要定义接口。
说明:值类型不能来自其他类型继承,但它们可以实现接口。例如,IComparable、IFormat table以及IConvertible都是接口,因此Int32、Int64等值类型及其他基本类型,都可以是
comparable、formattable及convertible的。
public?struct?Int32?:?IComparable,?IFormattable,?IConvertible{}
public?struct?Int64?:?IComparable,?IFormattable,?IConvertible{}
?
ü?考虑通过定义接口来达到与多重继承相类似的效果。
??不要在抽象类型中定义公有的或内部受保护的( protected-internal)构造函数。
说明:
只有当用户需要创建一个类型的实例时,该类型的构造函数才应该是公有的。由于你
无法创建一个抽象类型的实例,因此如果抽象类型具有公有构造函数,那么这样的设
计不仅是不当的,而且还会误导用户。
// bad design
public?abstract?class?Claim
{
public?Claim(int?number){}
}
// good design
public?abstract?class?Claim
{
// incorrect Design
protected?Claim(int?number){}
}
?
ü?要为抽象类定义受保护的构造函数或内部构造函数。
说明:受保护的构造函数更为常见,它仅仅是允许当子类型被创建时,基类能够做自己的初始化。
public?abstract?class?Claim
{
protected?Claim() { }
}
内部构造函数可以用来把该抽象类的具体实现限制在定义该抽象类的程序集中。
public?abstract?class?Claim
{
internal?Claim() { }
}
?
ü?要为你发布的抽象类提供至少一个继承自该类的具体类型。
说明:这有助于验证该抽象类的设计是否正确。例如,System.IO.FileStream是System.IO,Stream抽象类的一个实现。
ü?要尽量少用静态类。
?
??不要把静态类当做杂物箱。每一个静态类都应该有其明确的目的。
?
??不要在静态类中声明或覆盖实例成员。
?
ü?要把静态类定义为密封的、抽象的,并添加一个私有的实例构造函数——如果你的编程语言没有内置对静态类的支持。
ü?要定义接口,如果你需要包括值类型在内的一组类型支持一些公共的API。
?
ü?考虑定义接口,如果你需要让已经自其他类型继承的类型支持该接口提供的功能。
?
??避免使用记号接口(没有成员的接口)。如果你需要给一个具备某特征(记号)的类做记号,一般来说,最好使用自定义attribute而不要使用接口。
?
ü?要为接口提供至少—个实现该接口的类型。
例如:这有助于验证接口的设计。例如,System.Collections.ArrayList是System.Collections.IList接口的一个实现。
?
ü?要为你定义的每个接口提供至少一个使用该接口的API(一个以该接口为参数的方法,或是一个类型为该接口的属性)。这有助于有助于验证接口的设计。
例如:List<T>.Sort使用了IComparer<T>接口。
?
??不要给已经发行的接口再添加成员。
说明:这样做会破坏该接口的实现。为了避免版本的问题,应该创建一个新的接口。
??不要为结构提供默认的构造函数。
?
ü?要确保当所有的实例数据都为零、false或null(如果合适)时,结构仍处于有效状态。这可以防止在创建一个结构的数组时创建出无效的实例。
例如:下面的结构设计得不正确。带参数的构造函数有意用来确保状态有效,但是在创建该结构的数组时,这个构造函数没有被执行,因此所有的实例字段都被初始化为0,而对该类型来说这并不是一个有效的状态。
//bad Design
public?struct?PositiveInteger
{
int?value;
public?PositiveInteger(int?value)
{
if?(value <= 0)
{?throw?new?ArgumentException(); }
this.value = value;
}
public?override?string?ToString()
{
return?value.ToString();
}
}
?
通过确保默认的状态(本例中的value宇段为0)是该类型的有效逻辑状态,这个问题
可以得到解决。
//good Design
public?struct?PositiveInteger
{
int?value;
public?PositiveInteger(int?value)
{
if?(value <= 0)
{?throw?new?ArgumentException(); }
this.value = value - 1;
}
public?override?string?ToString()
{
return?(value + 1).ToString();
}
}
?
ü?要为值类型实现IEquatable<T>
说明:值类型的Object.Equals方法会导致装箱,而且它的默认实现也并不非常高效,因为它使用了反射。IEquatable<T>.Equals的性能要好得多,而且能够实现为不会导致装箱。
?
??不要显式地扩展System.ValueType,事实上大多数编程语言不允许这样做。
ü?要用枚举来加强那些表示值的集合的参数、属性以及返回值的类型性。
?
ü?要有限使用枚举而不要使用静态常量。
//Avoid the following
public?static?class?Color
{
public?static?int?Red = 0;
public?static?int?Green = 1;
public?static?int?Blue = 2;
}
//Favor the following
public?enum?Color
{
Red,
Grean,
Blue
}
?
??不要把枚举用于开放的集合(比如操作系统的版本、朋友的名字等)。
?
??不要提供为了今后使用而保留的枚举值。
?
??避免显式暴露只有一个值的枚举。
?
??不要把sentinel值包含在枚举值中。
例如,下面的代码显示了一个带sentinel值的枚举,这个附加的sentinel值用来表示枚举的最后一个值,其目的是用于范围检查。在框架设计中,这是不好的做法。
public?enum?DeskType
{
Circular = 1,
Oblong = 2,
Rectangular = 3,
LastValue = 3?//this sentinel should not be here
}
?
public?void?OrderDesk(DeskType?desk)
{
if?(desk >?DeskType.LastValue)
{?throw?new?ArgumentOutOfRangeException(); }
}
?
ü?框架的开发人员应该用真实的枚举值来执行检查,而不应该依赖于sentinel值。
public?void?OrderDesk(DeskType?desk)
{
if?(desk >?DeskType.Rectangular||desk<DeskType.Circular)
{?throw?new?ArgumentOutOfRangeException(); }
}
?
ü?要为简单枚举类型提供零值。
例如:可以考虑把该值称为“None”之类的东西。如果这样的值不适用于某个特定的枚举那么应该把该枚举中最常用的默认值赋值为零。
public?enum?Compression
{
None = 0,
GZip,
Deflate
}
public?enum?EventType
{
Error = 0,
Warning,
Information
}
?
ü?考虑用Int32(大多数编程语言的默认选择)作为枚举的基本实现类型,除非下面的
任何一条成立:
??该枚举是一个标记枚举,而你有超过32个标记,或预计今后会有更多的标记。
??为了更方便地与需要不同大小的枚举的非托管代码进行互操作,基本的实现类型必须是Int32之外的类型。
??更小的底层实现类型可能会节省相当的空间。
?
ü?要用复数名词或名词短语来命名标记枚举,用单数名词或名词短语来命名简单枚举。
?
??不要直接扩充System.Enum。
说明:System.Enurn是一个特殊的类型,被CLR用来创建用户定义的枚举。大多数编程语言提供了相应的编程元素让你访问这项功能。例如,在C#中,enum关键字被用来定义一个枚举。
ü?要对标记枚举使用System.FlagsAttribute。不要把该attribute用于简单枚举。
[Flags]
public?enum?AttributeTargets
{ }
?
ü?要用2的幂次方作为标记枚举的值,这样就可以通过按位或操作自由地组合它们。
[Flags]
public?enum?WatcherChangeTypes
{
Created = 0x0002,
Deleted = 0x0004,
Changed = 0x0008,
}
?