枚举
C#语言中的枚举是一种值类型,枚举允许程序员自定义命名值,对于一些表达某种类型、某种模式的变量,使用枚举来实现能够极大提高代码的可读性和可维护性。和直接用整数常量(魔法数字)相比,枚举具有类型安全、可读性好、便于IDE提示等优点。C#中,枚举底层默认使用int实现,但也可以指定其它整数底层类型。本篇文章我们介绍枚举的声明、使用、与整数和字符串的转换,以及位标志枚举的用法。
枚举声明
C#语言中,我们使用enum关键字声明枚举类型,具体的命名值使用逗号分隔。
enum CustomerType
{
Vip,
Normal
}
上面代码声明了一个名为CustomerType的枚举,包含Vip和Normal两个成员。每个枚举成员内部都对应一个整数值,默认从0开始,之后依次递增,因此Vip的值为0,Normal的值为1。
显式指定成员值
我们可以显式指定枚举成员的值,未指定的成员则继续在前一个成员的值上加1。
enum CustomerType
{
Vip = 10,
Normal, // 11
Blocked = 20
}
枚举成员的值允许重复,当两个成员的值相同时,它们互为别名。此时格式化输出得到的名称不确定(通常是其中某一个),因此实际开发中应尽量避免定义重复值的成员。
指定底层类型
枚举底层默认使用int实现,我们也可以使用冒号指定其它整型作为底层类型。可用的底层类型包括byte、sbyte、short、ushort、int、uint、long、ulong。
enum Status : byte
{
Disabled = 0,
Enabled = 1
}
当枚举成员数量较少时,使用byte作为底层类型可以节省内存,例如在网络通信、序列化等对内存布局敏感的场景中可能会用到。
枚举的使用
枚举的使用非常简单,和所有值类型相同,直接声明即可创建。
CustomerType t = CustomerType.Vip;
Console.WriteLine(t); // 输出 Vip
枚举变量也可以像其它值类型一样使用默认值,此时其值为0,即底层数值为0的那个成员。如果没有任何成员被赋值为0,默认值0依然可以被赋值给枚举变量,只不过它对应不上任何已定义的成员,这是使用枚举时需要留意的细节。
枚举最常见的用法是配合switch进行分支判断,相比判断整数或字符串更加清晰、安全。
CustomerType t = CustomerType.Vip;
switch (t)
{
case CustomerType.Vip:
Console.WriteLine("VIP用户");
break;
case CustomerType.Normal:
Console.WriteLine("普通用户");
break;
default:
Console.WriteLine("未知类型");
break;
}
枚举可以直接使用==、!=进行比较,也可以放进switch的分支标签。但要注意,枚举不支持关系运算符(<、>等),如果需要比较大小,需要先转换为底层整数类型再比较。
if ((int)CustomerType.Normal > (int)CustomerType.Vip)
{
Console.WriteLine("Normal的值更大");
}
枚举与整数、字符串的转换
枚举在本质上是一个整数,因此可以在枚举和整数之间进行显式转换。
// 枚举转整数
int i = (int)CustomerType.Normal; // 1
// 整数转枚举
CustomerType t = (CustomerType)1; // Normal
注意整数转枚举时不会做有效性校验,即使传入一个没有对应成员的值也不会报错,因此从外部输入(如数据库、网络)还原枚举时应当先校验。
枚举和字符串之间的转换则主要依靠Enum类提供的静态方法,ToString()可以将枚举转换为成员名,反向解析则使用Enum.Parse()或Enum.TryParse()。
// 枚举转字符串
string name = CustomerType.Vip.ToString(); // "Vip"
// 字符串转枚举,解析失败会抛出异常
CustomerType t1 = Enum.Parse<CustomerType>("Vip");
// 字符串转枚举,解析失败返回false而不抛出异常,推荐使用
if (Enum.TryParse("Vip", out CustomerType t2))
{
Console.WriteLine(t2); // Vip
}
TryParse在解析失败时返回false而不抛异常,实际开发中推荐使用,但需要注意两点:一是TryParse对形如"10"的数字字符串也会解析成功(等价于直接按数值转换);二是默认区分大小写,若需要忽略大小写可以传入第二个参数ignoreCase: true。
此外,Enum类还提供了一些遍历和校验枚举的常用方法,例子如下。
// 获取所有枚举成员的值
foreach (CustomerType t in Enum.GetValues<CustomerType>())
{
Console.WriteLine(t);
}
// 获取所有枚举成员的名称
string[] names = Enum.GetNames<CustomerType>();
// 判断某个值或名称是否为已定义的枚举成员
bool defined = Enum.IsDefined(CustomerType.Vip);
位标志枚举
有时我们希望一个枚举变量能够同时表示多个选项,例如文件的读写权限,可以同时拥有“读”和“写”两种权限。这时可以使用位标志枚举,也就是配合[Flags]特性(Attribute)的枚举。
位标志枚举的成员值应当是2的幂(1、2、4、8等),这样可以让每个成员对应一个二进制位,多个成员就可以通过按位或运算|组合起来。
[Flags]
enum FilePermissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
组合和判断权限的写法如下。
// 组合多个权限
FilePermissions access = FilePermissions.Read | FilePermissions.Write;
Console.WriteLine(access); // 输出 Read, Write
// 使用HasFlag判断是否包含某个权限
bool canRead = access.HasFlag(FilePermissions.Read); // true
// 也可以使用按位与运算判断
bool canExecute = (access & FilePermissions.Execute) == FilePermissions.Execute; // false
这里要注意几点:
- 位标志枚举应当定义一个值为
0的成员,通常命名为None,表示“没有任何选项”。 [Flags]特性本身并不会改变枚举的运算行为,按位运算无论是否加该特性都能工作,它的主要作用是影响ToString()的格式化结果,加上[Flags]后,组合值会被格式化为Read, Write这样的成员名列表,而不是单纯的数字。- 命名规范上,普通的枚举类型名一般用单数,如
CustomerType,而位标志枚举由于表示多个选项组合,通常用复数,如FilePermissions、Permissions。 HasFlag()方法使用简单,但它接收的参数是Enum类型,会带来装箱开销,在对性能敏感的代码中可以使用按位与运算替代。