How can I prevent bitwise OR combinations of enum values?
Asked Answered
C

4

24

I know that you can use FlagsAttribute to instruct the compiler to use bitfields for an enumeration.

Is there a way to specify that enum values cannot be combined with bitwise OR?

Example:

enum OnlyOneOption
{
   Option1,
   Option2,
   ...
}

In this example there is nothing stopping a developer from writing OnlyOneOption.Option1 | OnlyOneOption.Option2. I would like to forbid it, at compile time if possible.

Corfu answered 22/7, 2016 at 9:7 Comment(4)
Very short answer: If type safety is very important, don't use enums at all. They are essentially named and categorised integers, not much more.Fao
What stakx is saying. Nothing is technically preventing someone from doing var onlyOneOption = (OnlyOneOption)-9999; because that's also a valid integer value. What you should rely on is the specific members and not the values. Just like CompuChip suggests in his answerKulak
You could use a struct or class instead of an enum.Proclitic
You haven't specified exactly what you're trying to accomplish with this, but I suspect that your requirement that only one option can be chosen at once indicates some kind of state. The State Pattern could be useful to you.Motivation
E
27

Compile-time check

Recently, Eric Lippert (one of the guys that worked on the C#-compiler while he was at Microsoft) blogged about his top 10 regrets about C#, number four being that

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.

So in principle, you cannot get the compiler to choke on

OnlyOneOption option = OnlyOneOption.Option1 | OnlyOneOption.Option2;

because in terms of integers, that operation looks perfectly fine. What you can do, as you indicate, is not provide the FlagsAttribute - which is already a good hint to developers.

Since you cannot overload operators on enums either, you have to resort to runtime checking.

Run-time check

What you can do, is any time you need the enum, check for exact equality and throw an exception when a combination of values is used. Quickest and cleanest way is to use a 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.");
}

Non-enum solution

If you do not insist on using an enum, you could resort to the classical (Java-ish) static pattern:

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();
}

and here OnlyOneOption option = OnlyOneOption.OptionA | OnlyOneOption.OptionB; will fail with error CS0019: "Operator '|' cannot be applied to operands of type 'OnlyOneOption' and 'OnlyOneOption'".

The downside is that you lose the ability to write switch statements because they actually require a compile-time constant conversion to int (I tried providing a static public implicit operator int that returns a readonly field but even that is not enough - "A constant value is expected").

Elbertine answered 22/7, 2016 at 9:25 Comment(2)
The downside of static patterns would be solved if (crossing fingers) pattern matching makes it into C#. I find it, quite frankly, a much better solution than current enums.Kalinda
@Kalinda that would be great, in fact the current problem would already be solved if switch would work with runtime constants too so that this code would compile: ideone.com/Qjm4kTElbertine
K
5

No, you can't prevent that. Not specifying the [Flags] attribute will not disallow a user from the following:

enum Option
{
    One = 1,
    Two,
    Three
}

var myOption = Option.One | Option.Two;

Furthermore, something perfectly legal is the following:

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

or

var myOption = (Option)33;

Is this a problem? Well no, not really; if your logic only considers options One, Two or Three then simply enforce it:

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

Do note that if you have (suspect) code that decides based upon the underlying value of the enumeration:

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

Then you are most definitely not using the right tool for the job; myOption = 0; or myOption = (Option)-999 would be a problem.

Kalinda answered 22/7, 2016 at 9:38 Comment(1)
And if they say "Option.One | Option.Two" they'll get "Option.Three", which is probably what nobody wants in this scenario.Jehial
X
2

Add validation

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;
Xylotomy answered 22/7, 2016 at 9:32 Comment(3)
HasFlags does not check equality. For what the OP is asking, it would be better to check .Any(x => e == x)Typhon
No, Any(x=>e == x) can resturn true, even if the value contains more than one enum added. Let's say MyEnum has 2 more values: Value4 = 4, Value6 = 6 and e = MyEnum.Value2 | MyEnum.Value4; .Count(x => e.HasFlag(x)) == 1; returns False. .Any(x => e == x); returns True.Xylotomy
Er... that's only because Value2 + Value4 = Value6. I.E. It is true. In your suggested code Value6 would always return false, even though it is a valid value. If you don't intend to create your enum as flags, you shouldn't pretend that you did.Typhon
C
0

If you have something like this:

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

If you do MyEnum.Value2 | MyEnum.Value3 it will come up to be Value4. That is because 1 | 2 = 3. If you increase the first number to be 100000000 for example and end up with:

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

You will still have the same problem because 100000001 | 100000002 = 100000004 and that is Value4`.


Solution

The solution is to pick numbers that when you AND or OR them in whatever combination you will never get any of the others. Pick this numbers:

// 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

No matter how you OR or AND this numbers you will never get another one. As a result pick your enum to be:

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

Lastly this will always be false if you OR the values in whatever combination: Enum.IsDefined(typeof(MyEnum), MyEnum.Value1 | MyEnum.Value2)

So you can create this helper function

bool IsEnumValid(MyEnum enumValue)
{
    return Enum.IsDefined(typeof(MyEnum), enumValue);
}
Clubhouse answered 28/1, 2022 at 5:3 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.