如何防止枚举值的按位或组合?

How can I prevent bitwise OR combinations of enum values?

我知道您可以使用 FlagsAttribute 指示编译器使用位域进行枚举。

有没有办法指定枚举值不能与按位或组合?

示例:

enum OnlyOneOption
{
   Option1,
   Option2,
   ...
}

在此示例中,没有什么可以阻止开发人员编写 OnlyOneOption.Option1 | OnlyOneOption.Option2。如果可能的话,我想在编译时禁止它。

编译时检查

最近,Eric Lippert(他在 Microsoft 期间从事 C# 编译器工作的人之一)blogged 关于他对 C# 的十大遗憾,排名第四的是

In C#, an enum is just a thin type-system wrapper over an underlying integral type. All operations on enums are specified as actually being operations on integers, and the names of enum values are like named constants.

所以原则上,你不能让编译器阻塞

OnlyOneOption option = OnlyOneOption.Option1 | OnlyOneOption.Option2;

因为就整数而言,该操作看起来非常好。正如您所指出的,您可以做的是 而不是 提供 FlagsAttribute - 这已经是对开发人员的一个很好的提示。

因为在 enums 上 you cannot overload operators,你必须求助于运行时检查。

运行-时间检查

你可以做的是,任何时候你需要枚举,检查是否完全相等,throw当使用值的组合时抛出一个异常。最快最干净的方法是使用 switch:

// Use the bit pattern to guarantee that e.g. OptionX | OptionY
// never accidentally ends up as another valid option.
enum OnlyOneOption { Option1 = 0x001, Option2 = 0x002, Option3 = 0x004, ... };

switch(option) {
  case OnlyOneOption.Option1: 
    // Only Option1 selected - handle it.
    break;

  case OnlyOneOption.Option2: 
    // Only Option2 selected - handle it.
    break;

  default:
    throw new InvalidOperationException("You cannot combine OnlyOneOption values.");
}

非枚举解决方案

如果您不坚持使用 enum,您可以求助于经典(Java-ish)静态模式:

class OnlyOneOption
{
    // Constructor is private so users cannot create their own instances.
    private OnlyOneOption() {} 

    public static OnlyOneOption OptionA = new OnlyOneOption();
    public static OnlyOneOption OptionB = new OnlyOneOption();
    public static OnlyOneOption OptionC = new OnlyOneOption();
}

这里 OnlyOneOption option = OnlyOneOption.OptionA | OnlyOneOption.OptionB; 将失败并出现错误 CS0019:“Operator '|'不能应用于 'OnlyOneOption' 和 'OnlyOneOption'".

类型的操作数

缺点是您无法编写 switch 语句,因为它们实际上需要编译时常量转换为 int(我尝试提供 static public implicit operator int returns 一个 readonly 字段,但即使这样还不够 - "A constant value is expected").

添加验证

enum MyEnum
{
  Value1 = 1,
  Value2 = 2
}

  MyEnum e = MyEnum.Value1 | MyEnum.Value2;
  var isValid = Enum.GetValues(typeof(MyEnum)).Cast<MyEnum>().Count(x => e.HasFlag(x)) == 1;

不,你无法阻止。不指定 [Flags] 属性不会禁止用户执行以下操作:

enum Option
{
    One = 1,
    Two,
    Three
}

var myOption = Option.One | Option.Two;

此外,以下内容完全合法:

var myOption = 0; //language quirk IMO, 0 being implicitly convertible to any enumeration type.

var myOption = (Option)33;

这是个问题吗?不,不是真的;如果您的逻辑只考虑选项 OneTwoThree 那么只需执行它:

public Foo Bar(Option myOption)
{
     switch (myOption)
     {
         case Option.One: ...
         case Option.Two: ...
         case Option.Three: ...
         default: //Not a valid option, act in consequence
     }
}

请注意,如果您有(可疑的)代码根据枚举的基础值决定:

if (myOption < Option.Three) { //expecting myOption to be either One or Two...

那么您肯定没有使用正确的工具来完成工作; myOption = 0;myOption = (Option)-999 会有问题。

如果你有这样的事情:

enum MyEnum
{
  Value1,
  Value2,
  Value3,
  Value4,
}

如果你这样做 MyEnum.Value2 | MyEnum.Value3,它会变成 Value4。那是因为1 | 2 = 3。例如,如果您将第一个数字增加到 100000000 并以:

enum MyEnum
{
  Value1=100000000,
  Value2,
  Value3,
  Value4,
}

您仍然会遇到同样的问题,因为 100000001 | 100000002 = 100000004 是 Value4`。


解决方案

解决方案是选择当您 ANDOR 它们以任何组合组合时您永远不会得到任何其他数字的数字。选择这个数字:

// 1000 0000 0000 0000 0000 0000 0000 0001 = 2,147,483,649
// 0100 0000 0000 0000 0000 0000 0000 0010 = 1,073,741,826
// 0010 0000 0000 0000 0000 0000 0000 0100 = 536,870,916
// 0001 0000 0000 0000 0000 0000 0000 1000 = 268,435,464
// 0000 1000 0000 0000 0000 0000 0001 0000 = 134,217,744
// 0000 0100 0000 0000 0000 0000 0010 0000 = 67,108,896
// 0000 0010 0000 0000 0000 0000 0100 0000 = 33,554,496
// 0000 0001 0000 0000 0000 0000 1000 0000 = 16,777,344
// 0000 0000 1000 0000 0000 0001 0000 0000 = 8,388,864
// 0000 0000 0100 0000 0000 0010 0000 0000 = 4,194,816 
// 0000 0000 0010 0000 0000 0100 0000 0000 = 2,098,176
// 0000 0000 0001 0000 0000 1000 0000 0000 = 1,050,624
// 0000 0000 0000 1000 0001 0000 0000 0000 = 528,384
// 0000 0000 0000 0100 0010 0000 0000 0000 = 270,336
// 0000 0000 0000 0010 0100 0000 0000 0000 = 147,456
// 0000 0000 0000 0001 1000 0000 0000 0000 = 98,304

无论您如何 ORAND 这个数字,您都永远不会得到另一个数字。因此,选择您的枚举为:

enum MyEnum {
    Value1 = 98304,
    Value2 = 147456,
    Value3 = 270336,
    // etc
}

最后,如果您以任何组合对值进行或运算,这将始终为假:Enum.IsDefined(typeof(MyEnum), MyEnum.Value1 | MyEnum.Value2)

所以你可以创建这个辅助函数

bool IsEnumValid(MyEnum enumValue)
{
    return Enum.IsDefined(typeof(MyEnum), enumValue);
}