C#中多选枚举Enum的实现
引入主题
枚举类型是我们经常使用的类型,但常规的枚举类型并不能满足我们一些特别的需求。
举个例子:
一周共七天,这个枚举很容易写出来:
public enum Week
{
None,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
但是啊,如果我们想要表示所有工作日,我们该怎样表示呢?这时就需要FlagsAttribute特性了。
多选枚举的声明
提示:大家可以参考一下微软的官方文档。FlagsAttribute 类。不要看官方文档的中文版,那个机器翻译版本会让你看得头昏脑胀。
现在,让我们开始吧!首先,看看上面的枚举例子如何变成多选枚举。
[Flags]
public enum Week
{
None = 0, // 0
Monday = 1, // 1 << 0
Tuesday = 2, // 1 << 1
Wednesday = 4, // 1 << 2
Thursday = 8, // 1 << 3
Friday = 16, // 1 << 4
Saturday = 32, // 1 << 5
Sunday = 64 // 1 << 6
}
从上方的例子中可以看到,使用 FlagsAttribute 特性,只需要在 enum 前加上 [Flags] 标签。
但与此同时,我们将枚举中每一项的值手动赋予了。如果你计算机基础过硬,你就可以看出来,0、1、2、4、8……这种数字的规律正如右边注释所示。那,为什么要这样赋值,我可以随意赋值吗?这个问题将在本文下一节揭晓。
当然,我也喜欢直接写右边注释中的表达式代替这些数字,因为这样更加直观,这种代码风格问题就见仁见智了。
注意:
如果你不太清楚右边注释,或者对二进制中的位运算以及移位运算不熟悉,请先学习一下位运算符和移位运算符(C# 参考)。
注意:
即使不使用 [Flags],枚举中的每一项也是可以手动赋值的,只是一般情况下,不需要这么做。参见:枚举(C# 参考)。
多选枚举的原理
我们知道在二进制中,每一位的数字只能是 0 或 1 ,这也是二进制成为计算机底层逻辑的重要原因:它可以与电路开关的“开”“关”两种状态相对于。
那么我们也可以将每一位的 0 或 1,看作枚举中一项是否被选中。那么用二进制中的位与枚举中的项一一对应,就可以实现多选枚举。
在本文的例子中,一周共七天,所以有7位二进制数即可与之一一对应。例如:
0000000,没有任何位为一,说明哪一天都没有选;
0000001,只有右起第一位为一,说明只选了周一;
1000001,右起第一、七位为一,说明选了周一和周日。
以 2 的幂定义枚举常量,即 1、2、4、8 等。这意味着组合枚举常量中的各个标志不会重叠。
多选枚举的基本使用
直接输出
首选,让我们看看直接输出多选枚举会怎样。
public static void Main()
{
Console.WriteLine("\nFlagsAttribute 值的所有可能组合:");
for( int i = 0; i <= 64; i++ )
Console.WriteLine( "{0,3} - {1:G}", i, (Week)i);
}
这个程序最终将输出:
FlagsAttribute 值的所有可能组合:
// 0 - None
// 1 - Monday
// 2 - Tuesday
// 3 - Monday, Tuesday
// 4 - Wednesday
// 5 - Monday, Wednesday
……
……
这个程序说明,整数可以被强制转换为对应的枚举类型值并显示它们的字符串。
枚举加选
/// <summary>
/// 以原本的选择为基础,选择更多的选项。
/// </summary>
/// <param name="w"> 原本的选择。</param>
/// <param name="_wArray"> 所有要加选的选项组成的数组。</param>
public static void Add(ref Week w, Week[] _wArray)
{
foreach (Week 0._w in _wArray)
{
w = w | _w;
}
}
可以像上面这样写,但是稍稍有些复杂,将数组改为所有要加选的选项按位或得到的值,就会简单很多。
/// <summary>
/// 以原本的选择为基础,选择更多的选项。
/// </summary>
/// <param name="w"> 原本的选择。</param>
/// <param name="_wArray"> 所有要加选的选项组成的数组。</param>
public static void Add(ref Week w, Week _w)
{
w = w | _w;
}
枚举减选
/// <summary>
/// 不再选择原本已经选择的一些选项。
/// </summary>
/// <param name="w"> 原本的选择。</param>
/// <param name="_wArray"> 所有要减选的选项按位或的结果。</param>
public static void Remove(ref Week w, Week _w)
{
w = w & ~_w;
}
例如,将星期一从枚举变量中去掉:
0100 0011
&1011 1111 (~WeekDays.Monday)取反
=0000 0011 这样就把星期一去掉啦!
判断包含
/// <summary>
/// 判断当前已选的所有选项中是否包含了目标选项。
/// </summary>
/// <param name="w"> 现有的选择。 </param>
/// <param name="_w"> 目标选项(可以说多选项按位或)。</param>
public static bool IsContain(Week w, Week _w)
{
return 0 != (w & _w);
}
此外,有一个原生函数可以实现多选枚举判断包含的功能。Enum.HasFlag(Enum) 方法。用法:
现有的选择.HasFlag(目标选项(可以说多选项按位或))
判断None
/// <summary>
/// 判断当前是否没有选择任何选项(也就是选择了枚举中值为0的选项,一般为None)。
/// </summary>
public static bool IsNone(Week w)
{
return w != Week.None;
}
注意事项
-
仅当要对枚举值执行按位运算(AND、OR、EXCLUSIVE OR)时,才使用FlagsAttribute特性。
-
请考虑当前任务是否真的想要多选枚举,如果有一个枚举组合是非常常用的,则可以考虑直接将它作为一个枚举项,而不是枚举项的组合。例如,如果您有一个用于文件 I/O 操作的枚举,其中包含枚举常量Read = 1和Write = 2,请考虑创建枚举常量ReadWrite = 3,它结合了Read和Write标志。此外,用于组合标志的按位 OR 运算在某些情况下可能被视为高级概念,简单任务不需要。
-
最好不要将枚举值设置为负数,因为许多标志位置可能设置为 1,这可能会使您的代码混乱并鼓励编码错误。
-
使用None为标志的名称枚举常量,其值为零。创建None枚举常量仍然是值得的。原因是默认情况下,用于枚举的内存由公共语言运行时初始化为零。因此,如果不定义值为零的常量,则枚举在创建时将包含非法值。
-
如果您的应用程序需要表示明显的默认情况,请考虑使用值为零的枚举常量来表示默认值。如果没有默认情况,请考虑使用值为零的枚举常量,这意味着没有任何其他枚举常量表示的情况。
-
不要仅仅为了反映枚举本身的状态而定义枚举值。例如,不要定义仅标记枚举结束的枚举常量。如果您需要确定枚举的最后一个值,请明确检查该值。此外,如果范围内的所有值都有效,您可以对第一个和最后一个枚举常量执行范围检查。
-
不要指定为将来使用而保留的枚举常量。
-
当您定义将枚举常量作为值的方法或属性时,请考虑验证该值。原因是您可以将数值转换为枚举类型,即使该数值未在枚举中定义。