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枚举常量仍然是值得的。原因是默认情况下,用于枚举的内存由公共语言运行时初始化为零。因此,如果不定义值为零的常量,则枚举在创建时将包含非法值。

  • 如果您的应用程序需要表示明显的默认情况,请考虑使用值为零的枚举常量来表示默认值。如果没有默认情况,请考虑使用值为零的枚举常量,这意味着没有任何其他枚举常量表示的情况。

  • 不要仅仅为了反映枚举本身的状态而定义枚举值。例如,不要定义仅标记枚举结束的枚举常量。如果您需要确定枚举的最后一个值,请明确检查该值。此外,如果范围内的所有值都有效,您可以对第一个和最后一个枚举常量执行范围检查。

  • 不要指定为将来使用而保留的枚举常量。

  • 当您定义将枚举常量作为值的方法或属性时,请考虑验证该值。原因是您可以将数值转换为枚举类型,即使该数值未在枚举中定义。


本文参考的文章
在枚举Enum中使用Flag多选组合值
FlagsAttribute类(.Net官方文档)