首页 > 开发 > 综合 > 正文

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

2024-07-21 02:19:53
字体:
来源:转载
供稿:网友


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值同enum class方式没有本质的差异,只是struct必须提供无参数构造函数,因此无法实现封闭值域。

参考:

[joshua01]

effective java programming language guide , joshua bloch, pearson education,2001.
java 高效编程指南(中文版),机械工业出版社,2002

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表