常量/枚举类型的表示
系统中常常有一些属性的属性值是固定的一组值,它们的值域是封闭的(有限数量),比如国家代码(每个国家具有唯一的代码,而在一定时期国家的数量是确定的)、性别类型(男、女)。在现代程序语言中,一种典型的表示方式是枚举类型(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)更高的效率,定义简单。C#的Enum还支持位(Bit)操作,对于用于标志的常量,允许常量的OR、AND、XOR、NOT等位操作,C#提供了内建的语言支持,通过FlagsAttribute即可使Enum具备位操作能力。例如.Net Framework的System.AttributeTargets属性,允许Attribute子类在设置AttributeUsage时可以组合AttributeTargets属性(参见.Net Framework SDK):
// System.AttributeTargets的定义
[Flags] //Flags属性表明枚举是一个标志枚举,允许位操作
public enum AttributeTargets {
Class,
Method,
... ...
}
// 使用Flags枚举
// 下面的定义表示CustomerAttribute可以作用在Class或者Method上
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method )]
public sealed class CustomAttribute : Attribute {
... ...
}
Enum的缺悴荒苁迪肿远ㄒ宓男形薹ㄌ峁┏A扛嗟氖粜裕还诖蠖嗍榭鱿拢庖丫愎涣恕num Class没有这种限制,虽然Enum Class本身并不设计为可以继承,但可以修改基类(System.Object)的行为以提供更加丰富的能力(如修改ToString方法,根据使用者的本地语言输出本地化的国家名称),也可以提供更多的属性 。例如我们提供一个候选的国家列表,除了能显示国家名称外,可以提供国家代码、语言代码信息。关于Enum Class更多的应用场景后面的节有详细的描述。
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
分享到:
相关推荐
通过反射,我们可以获取枚举类的所有常量,如`Class.getEnumConstants()`。 11. **枚举的枚举常量枚举** 在枚举内部,可以通过`Enum<E extends Enum<E>>`的泛型方法`getDeclaringClass()`获取枚举常量所属的枚举...
在C++中,枚举类型分为不限定作用域(enum)和限定作用域(enum class)。 2. enum与enum class的区别? (为什么需要限定作用域?) 答:枚举作用域是指枚举类型成员名字的作用域,起自其声明之处,终止枚举定义...
本项目"test-enum-demo-master"显然是一个用于演示如何通过反射来操作枚举类的示例。 首先,让我们理解枚举类的基本概念。枚举类在Java中用于定义一组相关的固定数量的值,这些值通常代表常量。例如: ```java ...
下面我们将深入探讨`enum`枚举类的使用方法,并通过具体的代码示例来加深理解。 1. **枚举的定义** 枚举类的定义通常以关键字`enum`开头,后跟枚举名,然后是大括号`{}`内的一系列枚举常量。例如,我们可以定义一...
Java 中枚举类 enum 的 values() 方法详解 Java 中的枚举类 enum 是一种特殊的类,通过使用 enum 关键字来定义。枚举类 enum 中有一个特殊的方法,values(),这个方法可以将枚举类转换为一个枚举类型的数组。通过...
特性通过`[AttributeClass]`语法添加到类、方法、属性等元素上。例如,我们可以定义一个描述特性: ```csharp [AttributeUsage(AttributeTargets.Field)] class DescriptionAttribute : Attribute { public ...
枚举类自动继承了`java.lang.Enum`类,因此可以拥有方法和字段,也可以实现接口。 枚举的优势在于它们的类型安全性。当枚举用于表示有限的、预定义的选项时,编译器会检查所有枚举常量的引用,确保只有预先声明的...
结构体(struct)是值类型,而类(class)是引用类型。这意味着: - 结构体按值传递,复制的是整个结构体的数据。 - 类按引用传递,复制的是指向对象的引用,修改引用副本不会影响原始对象。 八、结构体的布局 ...
然后,在`OrderStatus`枚举类上使用`@JsonSerialize`注解指定使用这个序列化器: ```java @JsonSerialize(using = EnumNameSerializer.class) public enum OrderStatus { ... } ``` 这样,当后端接收到前端的枚举...
在编程中,`enum`(枚举)类型是用来定义一组命名的整数常量,它在C++中被广泛使用。然而,在某些情况下,我们可能会遇到`enum`类型的重定义问题,这通常发生在包含不同库或者头文件时,因为这些库可能已经定义了...
1. 创建枚举类:首先,定义一个枚举类,例如`Gender`,包含`MALE`和`FEMALE`两个枚举值,每个枚举值通常会有对应的描述。 ```java public enum Gender { MALE("男"), FEMALE("女"); private String description...
this.enumClass = enumClass; } @Override public void write(JsonWriter out, T value) throws IOException { if (value == null) { out.nullValue(); } else { out.value(value.name()); } } @...
在Java编程语言中,枚举(Enum)是一种特殊的类,用于定义一组预定义的常量。枚举类型在很多场景下被广泛使用,比如表示状态、权限、颜色等固定值集合。然而,当涉及到与数据库交互或者需要进行JSON序列化和反序列化...
枚举定义用class关键字,继承Enum类。 注意: 定义枚举时,成员名称不允许重复 默认情况下,不同的成员值允许相同。但是两个相同值的成员,第二个成员的名称被视作第一个成员的别名 如果枚举中存在相同值的成员,...
此外,如果需要处理其他类型枚举的反向赋值,只需调用`EnumUtil.getByDescription`方法,并传入相应的枚举类即可。 总结起来,通过创建一个枚举工具类,我们可以实现从枚举的描述反向赋值给实体类,简化了处理表单...
在 Java 中,枚举(enum)是一种特殊的类,它被用来表示一组固定的常量值。枚举类型自 JDK 1.5 开始引入,为开发者提供了一种更加安全、简洁的方式来处理一组固定的数据集合。枚举中的每个元素都是该枚举类型的对象...
为了实现这一目标,我们可以创建一个枚举类,例如: ```php class UserStatus { const ACTIVE = 'active'; const DISABLED = 'disabled'; } ``` 接下来,我们可以在Eloquent模型中定义一个属性访问器和修改器,...
在这里,`Color`是一个枚举类,包含三个预定义的实例:`RED`、`GREEN`和`BLUE`。它们都是`Color`类型的对象,可以直接使用。 在Spring Boot中,枚举常用于表示固定选项,例如状态、角色等。以下是如何在枚举中定义...
首先,枚举(Enum)在C#中是一种强大的工具,用于定义一组相关的常量。它们有助于提高代码的可读性和可维护性。例如,我们可以创建一个名为`Color`的枚举,包含`Red`, `Green`, `Blue`等颜色常量。在WPF应用中,枚举...