第3章 类型设计规范
类型从逻辑上分组为。
ü 要确保每个类型由一组定义明确、相互关联的成员组成。
3.1. 类型和名字空间
ü 要用命名空间把类型组织成一个相关的特性域的层次结构。
û 避免非常深的名字空间层次。这样的层次难于浏览,因为用户不得不经常地回溯。
û 避免有太多的名字空间。
û 避免把为高级场景而设计的类型和为常见编程任务而设计的类型放在同一个名字空间中。
û 不要不指定名字空间就定义类型。
3.2. 类和结构之间的选择
ü 要深入了解引用类型和值类型在行为上的差异。
ü 作为一条经验法则,框架中的大多数类型应该是类。但是在有些情况下,由于值类型所具备的特征,使用结构会更为合适。
ü如果该类型的实例比较小而且生命期比较短,或者经常被内嵌在其他对象中,考虑定义结构而不要定义类。
û 不要定义结构,除非该类型具有以下所有特征:
Ø 它在逻辑上代表一个独立的值,与基本类型(int、double等等)相似。
Ø 它的实例的大小小于16个字节。
Ø 它是不可变的。
Ø 它不需要经常被装箱。
在所有其他情况下,应该将类型定义为类。
3.3. 类和接口之间的选择
ü 要优先采用类而不是接口。
说明:与基于接口的API相比,基于类的API容易演化得多,因为可以给类添加成员而不会破坏已有的代码。
ü 要用抽象类而不是用接口来解除协定与实现之间的耦合。
说明:抽象类经过正确的设计,同样能够解除协定与实现之间的耦合,与接口所能达到的程度不相上下。
ü 如果需要提供一个多态的值类型层次结构的话,要定义接口。
说明:值类型不能来自其他类型继承,但它们可以实现接口。例如,IComparable、IFormat table以及IConvertible都是接口,因此Int32、Int64等值类型及其他基本类型,都可以是
comparable、formattable及convertible的。
public struct Int32 : IComparable,IFormattable,IConvertible{}
public struct Int64 : IComparable,IConvertible{}
ü 考虑通过定义接口来达到与多重继承相类似的效果。
3.4. 抽象类的设计
û 不要在抽象类型中定义公有的或内部受保护的( 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抽象类的一个实现。
3.5. 静态类的设计
ü 要尽量少用静态类。
û 不要把静态类当做杂物箱。每一个静态类都应该有其明确的目的。
û 不要在静态类中声明或覆盖实例成员。
ü 要把静态类定义为密封的、抽象的,并添加一个私有的实例构造函数——如果你的编程语言没有内置对静态类的支持。
3.6. 接口的设计
ü 要定义接口,如果你需要包括值类型在内的一组类型支持一些公共的API。
ü 考虑定义接口,如果你需要让已经自其他类型继承的类型支持该接口提供的功能。
û 避免使用记号接口(没有成员的接口)。如果你需要给一个具备某特征(记号)的类做记号,一般来说,最好使用自定义attribute而不要使用接口。
ü 要为接口提供至少—个实现该接口的类型。
例如:这有助于验证接口的设计。例如,System.Collections.ArrayList是System.Collections.IList接口的一个实现。
ü 要为你定义的每个接口提供至少一个使用该接口的API(一个以该接口为参数的方法,或是一个类型为该接口的属性)。这有助于有助于验证接口的设计。
例如:List<T>.Sort使用了IComparer<T>接口。
û 不要给已经发行的接口再添加成员。
说明:这样做会破坏该接口的实现。为了避免版本的问题,应该创建一个新的接口。
3.7. 结构的设计
û 不要为结构提供默认的构造函数。
ü 要确保当所有的实例数据都为零、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,事实上大多数编程语言不允许这样做。
3.8. 枚举的设计
ü 要用枚举来加强那些表示值的集合的参数、属性以及返回值的类型性。
ü 要有限使用枚举而不要使用静态常量。
//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关键字被用来定义一个枚举。
3.8.1. 标记枚举的设计
ü 要对标记枚举使用System.FlagsAttribute。不要把该attribute用于简单枚举。
[Flags]
public enum AttributeTargets
{ }
ü 要用2的幂次方作为标记枚举的值,这样就可以通过按位或操作自由地组合它们。
[Flags]
public enum WatcherChangeTypes
{
Created = 0x0002,
Deleted = 0x0004,
Changed = 0x0008,
Renamed = 0xoo10
}
ü 位操作是一个高级概念,对简单的任务来说应该不是必需的。FileAccess.ReadWrite
就是这样的一个例子。
[Flags]
public enum FileAccess
{
Read = 1,
Write = 2,
ReadWrite = Read | Write
}
û 避免让创建的标记枚举包含某些无效的组合。
û避免把零用作标记枚举的值,除非该值表示“所有标记都被清除”,而且按下一条规范进行适当的命名。
例如:下面显示了一个常见的实现方法,程序员们用它来判断一个标记是否被设置(见下面的if语句)。该检测对标记枚举所有的值(除了枚举值零)都能够如预期的那样工作,在枚举值为零情况下该布尔表达式的值始终为真。
[Flags]
public enum SomeFlag
{
ValueA = 0,//this might be confusing to users
ValueB = 1,
ValueC = 2,
ValueBAndC = ValueB | ValueC
}
SomeFlag flags = GetValue();
if((flags&SomeFlag.ValueA)==SomeFlag.ValueA)
{}
ü 要把标记枚举的零值命名为None。对标记枚举来说,该值必须始终意味着“所有标记均被清除”。
public enum BorderStyle
{
Fixed3D = Ox1,
FixedSingle = Ox2,
None = Ox0
}
if (foo.BorderStyle == BorderStyle.None)
{}
3.8.2. 给枚举添加值
ü 考虑给枚举添加值,尽管有那么一点兼容性的风险。
说明:如果你有实际数据,表明给枚举添加值会导致应用程序的不兼容,那么可以考虑添加一个新的API来返回新老枚举值,而老的API(仍返回老的枚举值)建议不再使用。这样就能确保仍然兼容现有的应用程序。
3.9. 嵌套类型
ü 要想让一个类型能够访问外层类型的成员时才使用嵌套类型。
û 不要使用嵌套类型来进行逻辑分组,应该用命名空间来达到此目的。
û 避免公开地暴露嵌套类型。唯一的例外是如果只需在极少数的场景中声明嵌套类型的变量,比如派生子类时,或者其他高级的自定义场景中。
û 不要使用嵌套类型,如果该类型可能会被除了它的外层类型之外的类型引用。
例如:如果一个枚举是某类的一个方法的参数,那么这个枚举不应该被定义为该类的嵌套类型。
û 如果它们需要被客户代码实例化,不要使用嵌套类型。如果一个类型具有公有构造函数,那么它可能不应该被嵌套在其他类型中。
û 不要把嵌套类型定义为接口的成员,许多编程语言不支持这种结构。一般来说,应该尽量少用嵌套类型,而且应该避免将嵌套类型公开暴露给外界。