C# Idioms: Enum还是Enum Class(枚举类)

** C# Idioms:Enum ** ** 还是 ** ** Enum Class( ** ** 枚举类 ** ** ) **

_ marshine _

_ (原文排版格式: http://www.marshine.com _ _ ) _

_reversion:2004/5/28
修改说明:感谢 _ Ninputer 提到的CLS兼容问题,同时修改了原来版本没有提及的Equals改写,以及修改"=="重载的不完善代码,和增加enum struct内容

reversion:2004/6/4

增加kirc提到的Enum的Flags特性,因为文本超长,新的版本可以在 http://www.marshine.com 上阅读。

__

** 常量类型的表示 ** ** **

系统中常常有一些属性的属性值是固定的一组值,它们的值域是封闭的(有限数量),比如国家代码(每个国家具有唯一的代码,而在一定时期国家的数量是确定的)、性别类型(男、女)。在现代 程序语言中,一种典型的表示方式是枚举类型( Enum )。 Enum 表示封闭值域的类型,常常由程序语言作为一种数据类型直接支持,例如 C , C# 等。 C# 支持的 enum 在 C 的基础上提供了类型安全的能力,下面是用 C# 定义的性别枚举类型:

public enum Sex {
Male,
Female,
}

Java 不支持 enum 数据类型, Java 认为 C 提供的 enum 并不是类型安全的,通常使用称之为 Typesafe Enum Class 的设计模式来获得类似的效果(参见 [Joshua01] P80,Item21 :Replace enum constructs with classes )。 Enum Class 不允许外部构造实例成员(构造函数为 private ),提供静态类型成员实例来表示封闭值域。使用 Enum Class 方式来表示 Sex 类型可定义如下( C# ):

public class Sex{
// 私有构造保证值域的封闭性
private Sex() {
}

pubic static readonly Sex Male = new Sex():
pubic static readonly Sex Female = new Sex():
}

同 enum 一样,可以使用 Sex.Male 或 Sex.Female 的方式来访问常量属性,与静态常量字段不一样(如静态字符串、整数), enum 和 Enum Class 可以提供强类型的 compile time 检查以及提供更好的数据封装性和代码可读性。例如使用常量类型设置和比较属性值:

// 设置属性值
Sex sex = Sex.Male;
// 比较
if (sex == Sex.Male) {
// ... ...
}

如果 Sex 是使用 Enum 定义的,则上面比较的实际上是 Enum 字段的值;如果 Sex 是使用 Enum Class 定义的,则比较的是静态实例成员的引用地址,当然也可以使用 Equals 方法来比较。

虽然 Enum Class 是来自于 Java 的设计模式,但在 C# 中并非没有意义,因为 Enum Class 提供了比 Enum 类型更强大的能力。

** Enum ** ** 与 ** ** Enum Class ** ** 的比较 ** ** **

Enum 与 Enum Class 均提供了封装常量的能力,都能够实现编译时的强类型检查,使用封闭值域防止非法值。不过,因为实现机制的不同,这两种方式也具有不同的特点。

Enum 在 C# 中是一种值类型( Value Type ),其基类型必须是整数类型(如 Int16 ),因此 Enum 也具有值类型所具有的优点 —— 比引用类型( Reference Type )更高的效率,定义简单。但是其缺点不能实现自定义的行为,无法提供常量更多的属性。

Enum Class 就没有这种限制,虽然 Enum Class 本身并不设计为可以继承,但可以修改基类( System.Object )的行为以提供更加丰富的能力(如修改 ToString 方法,根据使用者的本地语言输出本地化的国家名称),也可以提供更多的属性 。例如我们提供一个候选的国家列表,除了能显示国家名称外,可以提供国家代码、语言代码信息。

** Enum Class ** ** 的问题 ** ** **

但 Enum Class 也有它的缺点,上面的设计中 Enum Class 通过进程内静态成员引用地址相同来进行比较,但是当将一个序列化后的 Enum Class 实例反序列化后, CLR 会创建一个新的实例,从而造成反序列化值不等于序列化前值的现象:

IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

MemoryStream stream = new MemoryStream();
// 序列化 Sex.Male 的值
formatter.Serialize(stream, Sex.Male);
stream.Seek(0,SeekOrigin.Begin);
// 反序列化
Sex sex = (Sex)formatter.Deserialize(stream);
Console.WriteLine(sex == Sex.Male);

上面的代码将输出 false 。因此通过引用的方式是有局限性的,在 Java 中这是一个比较棘手的问题,需要修改反序列化的行为(参看 [Joshua01]P171 )。 C# 与 Java 的实现机制不一样,无法通过修改反序列化的行为来返回同一个常量实例, 但 C# 提供了操作符重载的能力,我们可以通过重载操作符 “==” 来解决这个问题,同时为了保持 CLS 兼容以及与 Equals 的行为一致,还需要改写 Equals 方法:

[Serializable]
public class Sex{
// 性别类型名
private string sexName;

// 私有构造保证值域的封闭性
private Sex(string sexName) {
this.sexName = sexName;
}

public static readonly Sex Male = new Sex("Male");
public static readonly Sex Female = new Sex("Female");

// 提供重载的 "==" 操作符,使用 sexName 来判断是否是相同的 Sex 类型
public static bool operator ==(Sex op1, Sex op2) {
if (Object.Equals(op1, null)) return Object.Equals(op2, null);
return op1.Equals(op2);
}

public static bool operator !=(Sex op1,Sex op2) {
return !(op1 == op2);
}

public override bool Equals(object obj) {
Sex sex = obj as Sex;
if (obj == null) return false;
return sexName == sex.sexName;
}

public override int GetHashCode() {
return sexName.GetHashCode ();
}
}

通过操作符重载,不再使用引用地址来比较常量,而是通过值比较(如上面的 sexName ),因此要求每个常量实例必须具有唯一的标识值。 在不支持操作符重载的语言中,不能使用 "==" 来比较两个常量值是否相等,而应该使用 Equals 方法来代替。

** Enum Class ** ** 的设计 ** ** **

Enum Class 一般符合下列规则:

  • 私有构造函数,保证外部无法创建类实例(同时也使得类无法继承)。
  • 静态只读实例字段表示常量。
  • 重载操作符 "==" ,保证序列化后的值也能比较相等。当需要在进程间传递(如分布式应用)或需要序列化时,必须实现 "==" 操作符的重载。
  • 改写 Equals 方法,保持 "==" 行为和 Equals 一致。(改写 Equals 一般也同时改写 GetHashCode 方法 )

除此之外,还通常改写 ToString 方法以提供显示友好的名字,因为 Java 和 .Net 都在绑定或显示对象时使用 ToString 方法( Java 中为 toString 方法)输出作为缺省的对象显示字符串,比如将 Sex 数组绑定到 ListBox 或者使用 Console.Write 输出时。下面的代码改写 ToString 方法以提供友好显示的输出:

public class Sex{
... ...
public override string ToString() {
return sexName;
}
}

当然我们也可以利用 ToString 提供本地化支持,返回本地语言的字符串。

Enum Class 另外一种常见的职责是提供不同值系统之间的类型转换,如当从数据库中读取值时,利用 Parse 方法将数据库中值转换为对象系统的常量实例,而在存储时提供方法转换为数据库的值类型:

public class Sex{
... ...
// 根据一个符合指定格式的字符串返回类型实例。
public static Sex Parse(string sexName){
switch (sexName) {
case "Male" : return Male;
... ...
}
}

// 返回数据存储的值。
public string ToDBValue(){
return sexName;
}
}

** 使用 ** ** Enum ** ** 还是 ** ** Enum Class ** ** ? ** ** **

根据 Enum 和 Enum Class 的特点,我们可以根据对常量类型的要求决定使用 Enum 还是 Enum Class 。

以下场景适合使用 Enum :

  • 常量类型用于内部表示,不用于显示名字。
  • 常量值不需要提供附加的属性。例如只需要知道国家代码,而不需要获得国家的其它属性

Enum Class 可以适用于更多的场景:

  • 常用于可提供友好信息的类型。如本地化支持的类型名显示,或者显示与枚举名不一致的名字,例如 Country.CHN 可显示为 "China" 。
  • 提供更多的常量属性。
  • 提供更加丰富的行为。如 Parse 方法。
  • 对常量进行分组。如 Country.Asia 包含亚洲国家。

** 使用 ** ** Struct ** ** 来表示枚举 ** ** **

如果值域不封闭,但希望提供一些常量,也可以使用 struct ,如 System.Drawing.Color 结构中的系统默认颜色设置。采用 struct 来设计 enum <SPAN style="FONT-SIZE: 9pt; COLOR: black; FO

Published At
Categories with Web编程
Tagged with
comments powered by Disqus