Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid impossible enum values?

Say I have the following enum:

[Flags]
enum IntFlags
{
  A = 1 << 0,
  B = 1 << 1,
  C = 1 << 2,

  AAndB = A | B
}

and I want to build a helper method that sets flags like the following:

private static IntFlags Set(IntFlags values, IntFlags target)
{
  return target | values;
}

Is there any flexible way to do some sanity checking? Like it should be impossible to set values that don't exist in the enum or are not a combination of existing values.

Stuff like the following should not be possible:

IntFlags flags = 0;
flags = Set(0, flags);
flags = Set((IntFlags)int.MaxValue, flags);
flags = Set(~IntFlags.A, flags);

I guess just doing a check against the existing values like:

private static IntFlags Set(IntFlags values, IntFlags target)
{
  if (!Enum.GetValues(typeof(IntFlags)).Cast<IntFlags>().Contains(values))
    throw new ArgumentException();

  return target | values;
}

won't work because setting values like

IntFlags flags = 0;
flags = Set(IntFlags.A | IntFlags.C, flags);

should be allowed.

I think this might work:

IntFlags Set(IntFlags values, IntFlags target)
{      
  int intValue;
  if (int.TryParse(values.ToString(), out intValue))
  {
    throw new ArgumentException();
  }

  ...
 }

but this relies on the fact that I'm using the Flags attribute on the enum.

Edit

I now found the Enum.IsDefined() method. It works nicely with

Assert.False(Enum.IsDefined(typeof(IntFlags), (IntFlags)int.MaxValue));
Assert.False(Enum.IsDefined(typeof(IntFlags), 0));
Assert.False(Enum.IsDefined(typeof(IntFlags), ~IntFlags.A));

but fails my requirements with combined values

Assert.True(Enum.IsDefined(typeof(IntFlags), IntFlags.A | IntFlags.C)); //fails
like image 681
user764754 Avatar asked Feb 13 '23 02:02

user764754


2 Answers

There's no way to statically prohibit users of an Enum from creating an enum representing any value of the underlying type. You can only do such validation at runtime, as you showed in the question.

If you're just looking for a simple enough way of validating, at runtime, if a particular enum value is a valid combination of flags, then that's a simple enough check, assuming the defined enum values are actually increasing powers of two. For such an enum all valid values will go from zero to one less than the next highest power of two, so the check would become:

if((int)values > 0 && (int)values >= 1 << 3))
    throw new ArgumentException();

Technically you could remove all possible unmapped values by must mapping every single value, but that's not a particularly practical solution.

All that you're left with if either of those aren't an option is to not use an Enum, but to instead use your own custom type that has the exact semantics you want:

public class IntFlags
{
    public int Value { get; private set; }
    private IntFlags(int value)
    {
        this.Value = value;
    }

    public static readonly IntFlags A = new IntFlags(1);
    public static readonly IntFlags B = new IntFlags(2);
    public static readonly IntFlags C = new IntFlags(4);

    public static IntFlags operator |(IntFlags first, IntFlags second)
    {
        return new IntFlags(first.Value | second.Value);
    }
    public override bool Equals(object obj)
    {
        return Equals(obj as IntFlags);
    }
    public override bool Equals(IntFlags obj)
    {
        return obj != null && Value == obj.Value;
    }
}
like image 77
Servy Avatar answered Feb 14 '23 17:02

Servy


Having a static IntFlag field where you actually add these values is a good solution and as you realized we do have a Enum.IsDefined but just FYI, you can go the bit masking way as well. I threw up something together in LINQPad to give you an idea:

BitVector32 is in System.Collections.Specialized

[Flags]
enum IntFlags
{
    None = 0,
    A = 1 << 0,
    B = 1 << 1,
    C = 1 << 2,

    AAndB = A | B
}

void Main()
{
    BitVector32 bv = new BitVector32(0);
    int mask1 = BitVector32.CreateMask();
    int mask2 = BitVector32.CreateMask(1 << 0);
    int mask3 = BitVector32.CreateMask(1 << 1);
    int mask4 = BitVector32.CreateMask(1 << 2);

    bv[mask1 + mask2 + mask3 + mask4] = true;

    //IntFlags flag1 = IntFlags.A | IntFlags.C;
    IntFlags flag1 = (IntFlags)int.MaxValue;
    if((bv.Data & (int)flag1) == (int)flag1)
        "Yes".Dump();
    else
        "No".Dump();
}
like image 39
Farhad Alizadeh Noori Avatar answered Feb 14 '23 17:02

Farhad Alizadeh Noori