枚举
枚举
枚举应该是我们平时自定义的最的值类型,枚举类型的本质其实就是静态的常量字段。看下面代码的Color枚举的IL代码。可以看出枚举的默认值是int,并且从0开始,递增1。下面介绍一些关于枚举类型的小技巧。
public enum Color
{
Red,
Green,
Blue
}
.class nested public sealed auto ansi
Color
extends [System.Runtime]System.Enum
{
.field public specialname rtspecialname int32 value__
.field public static literal valuetype EnumTest.Program/Color Red = int32(0)
.field public static literal valuetype EnumTest.Program/Color Green = int32(1)
.field public static literal valuetype EnumTest.Program/Color Blue = int32(2)
}
修改基础类型
枚举默认的默认基础类型是int类型,我们可以通过继承Byte或者手动赋值为到其他类型的形式修改基础类型。
public enum Color: byte
{
Red,
Green,
Blue
}
.class nested public sealed auto ansi
Color
extends [System.Runtime]System.Enum
{
.field public specialname rtspecialname unsigned int8 value__
.field public static literal valuetype EnumTest.Program/Color Red = unsigned int8(0) // 0x00
.field public static literal valuetype EnumTest.Program/Color Green = unsigned int8(1) // 0x01
.field public static literal valuetype EnumTest.Program/Color Blue = unsigned int8(2) // 0x02
}
使用大量的基础类型可以节约更小的内存(byte 只有1字节),如果你的枚举作为字段或数组需要配大量的创建和存储,或者在网络传输中最求高性能可以考虑使用更小的基础类型。如果只是作为控制判断那么影响甚微。当然了你还需要考虑值范围的问题,byte类型范围是0到255。
为枚举增加特性和扩展方法
有的时候我们需要为枚举字段添注释,那么可以使用Description特性。
public enum SetOperation
{
[Description("Add to Set")]
SADD,
[Description("Remove from Set")]
SREM,
}
我们无法在枚举中直接添加方法,不过可以使用扩展方法来对枚举进行操作。
获取Description的值
下面是使用反射来获取特性的值,性能较差。
public static string GetDescription(this Enum value)
{
var field = value.GetType().GetField(value.ToString());
DescriptionAttribute? attribute = field?.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute;
return attribute == null ? value.ToString() : attribute.Description;
}
调用方式非常简单:
SetOperation.SADD.GetDescription()
判断枚举的合法性
我们指定枚举类型的基元类型是值类型,可以使用范围判断的方式来检查值的合法性。有规律的枚举值都建议使用自定义的校验方法来校验枚举值的合法性。
public static bool IsDefinedSetOperation(SetOperation value)
{
return value >= SetOperation.SADD && value <= SetOperation.SREM;
}
public static bool IsDefinedSetOperation(int value)
{
return value >= (int)SetOperation.SADD && value <= (int)SetOperation.SREM;
}
对于无规律的可以使用IsDefined来校验,此方法可以接受object,字符串会获取枚举所有的名称,整数则会获取所有的值并通过二分查找判断。
Type valueType = value.GetType();
// 是否为枚举类型
if (valueType.IsEnum)
{
if (!valueType.IsEquivalentTo(this))
throw new ArgumentException(SR.Format(SR.Arg_EnumAndObjectMustBeSameType, valueType, this));
valueType = valueType.GetEnumUnderlyingType();
}
// 字符串处理
if (valueType == typeof(string))
{
string[] names = GetEnumNames();
if (Array.IndexOf(names, value) >= 0)
return true;
else
return false;
}
// 整数处理
if (IsIntegerType(valueType))
{9
Type underlyingType = GetEnumUnderlyingType();
// 查传入值的类型是否与枚举的基础类型相同
if (underlyingType.GetTypeCodeImpl() != valueType.GetTypeCodeImpl())
throw new ArgumentException(SR.Format(SR.Arg_EnumUnderlyingTypeAndObjectMustBeSameType, valueType, underlyingType));
Array values = GetEnumRawConstantValues();
// 此方法会把值转换为ulong的数组并调用Array.BinarySearch(),进行二分查找
return BinarySearch(values, value) >= 0;
}
else
{
// 不为整数或字符串抛出异常
throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType);
}
从性能角度来说,应当为枚举类单独写IsDefined方法。获取所有枚举值这个也是一样。\
使用枚举来代替固定的int或bool
我们经常在方法中传递返回true或flase,0或1这样的数据来表示特殊的含义,我们可以用枚举来代替其怎加可读性。也带来更强的可维护性,后续增加枚举的字段非常的简单。
例如:添加一条不重要的代办(这个例子不是很好)。
ToDo.Add("do ...",false);
ToDo.Add("do ...",ImportanceLeve.General);
在维护中经常会为了不破坏现有传递逻辑,在方法传参的最后添加有默认值的bool或者int,如果使用枚举既可以怎加代码的可读性,也方便了后续的维护。
Flags 标记枚举
Flags 特性允许将枚举值视为位域(bit fields)。这意味着可以将多个枚举值组合在一起,通过按位运算(如按位或 |)来表示多个状态或选项。下面看 System.IO.FileAccess的源码。
/// <summary>Defines constants for read, write, or read/write access to a file.</summary>
[Flags]
public enum FileAccess
{
/// <summary>Read access to the file. Data can be read from the file. Combine with <see langword="Write" /> for read/write access.</summary>
Read = 1,
/// <summary>Write access to the file. Data can be written to the file. Combine with <see langword="Read" /> for read/write access.</summary>
Write = 2,
/// <summary>Read and write access to the file. Data can be written to and read from the file.</summary>
ReadWrite = Write | Read, // 0x00000003
}
按位或 |
按位或运算符 | 用于将两个数的二进制位进行或运算。它的作用是,如果两个数的对应位中有一个是 1,则结果位为 1;否则为 0。所有数和0按位或都会等于他本身,因为0的二进制不论几位都是0。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int result = a | b; // 结果: 0111 (十进制: 7)
这个标记标记枚举用于组合多个标记,ReadWrite = Write | Read也是1|2结果为3。
如果我们想增加一个枚举是值Execute = 3,来替换ReadWrite枚举会发生啥情况呢?那就是FileAccess.Read | FileAccess.Write和Execute 发生冲突,两者的值都是3。
我们应该使用2的幂作为标记枚举的值。可以确保每个标记在二进制表示中只有一个位是 1。例如:
-
1 (二进制: 0001)
-
2 (二进制: 0010)
-
4 (二进制: 0100)
-
8 (二进制: 1000)
这样,当你使用按位或运算符 | 组合这些标记时,每个位都可以独立表示一个标记,不会发生冲突。
按位与 &
按位与运算符 & 用于将两个数的二进制位进行与运算。它的作用是,如果两个数的对应位都是 1,则结果位为 1;否则为 0。所有数和0按位或都会等于0。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int result = a & b; // 结果: 0001 (十进制: 1)
在标记枚举里可以用于检查某个标志是否被设置。
var accessWrite = FileAccess.Write;
var accessRead = FileAccess.Read;
var access = FileAccess.Read | FileAccess.Write;
Console.WriteLine((accessWrite & FileAccess.Read) == FileAccess.Read); // False
Console.WriteLine((accessRead & FileAccess.Read) == FileAccess.Read); // True
Console.WriteLine((access & FileAccess.Read) == FileAccess.Read); // True
使用按位与 & 和按位取反 ~ 运算符可以清除某个标志。
// access组合中去除Write权限
var access = access & ~FileAccess.Write;
使用左移来展示更加清晰的标记枚举
左移的意思就是:每一位都向左移动指定的位数,右边用 0 填充。例如0001向左移动2
1 << 2这个代码中,1是进行左移操作的数,2是要左移的位数。1的二进制是0001左移2位就是000100,也就是0100十进制的4。1的每一次左移都是2的幂,所有可以用来便捷生成标记枚举的值。
[Flags]
public enum FileAccess
{
None=0,
Read = 1 << 1, // 二进制: 0010 十进制: 2
Write = 1 << 2, // 二进制: 0100 十进制: 4
Execute = 1 << 3, // 二进制: 1000 十进制: 8
}

浙公网安备 33010602011771号