9

Consider the following piece of code:

namespace ConsoleApplication1 {
class Program {
    public static void Main (string[] args) {
        var en = (TestEnum)Enum.Parse(typeof(TestEnum), "AA");
        Console.WriteLine(en.ToString());
        Console.ReadKey();
    }
}

public enum TestEnum {
    AA = 0x01,
    AB = 0x02, 
    AC = 0x03,
    BA = 0x01,
    BB = 0x02,
    BC = 0x03
}
}

If you execute this, the variable en will get the value of TestEnum.BA. Now I have learned from this that enum flags should be unique, or you get these kind of unexpected things, but I do fail to understand what is happening here.

The even weirder part is that when I add the [Flags] attribute to the TestEnum, it solves the problem and returns TestEnum.AA instead of TestEnum.BA, but for the original enum (which is much larger, around ~200 members) for which I have discovered this problem this does not make a difference.

My understanding is that enums are a value type, so when you define your own flags it will store the value in memory as 0x01 in the case of for TestEnum.AA, and when you cast it from object to TestEnum it will do the lookup for that flag value and find TestEnum.BA.

This is also confirmed by running the following line:

var en = (TestEnum)(object)TestEnum.AA;
Console.WriteLine(en.ToString());

Which will output: BA

So my question is: what exactly is happening here? And more importantly why does adding the Flags attribute make a difference?

larzz11
  • 1,022
  • 2
  • 11
  • 24
  • 1
    As they have the same value, it will pick one pretty arbitrarily. [This related question](https://stackoverflow.com/questions/8043027/non-unique-enum-values) might help. – Charles Mager Jul 04 '17 at 08:21
  • I don't know the internal workings of an enum, so I won't try to answer here (there will be more capable people who can do that), but as a matter of a fact, the `[Flags]` attribute, **only** has any importance on the `ToString()` part (on the formatting)... otherwise, the enum works exactly the same – Jcl Jul 04 '17 at 08:22
  • 1
    Adding `[Flags]` likely just changes the algorithm used to determine the correct value. Both answers are actually correct. – DavidG Jul 04 '17 at 08:30
  • Try the following with & without [Flags] : Console.WriteLine(TestEnum.AA.ToString()); Console.WriteLine(TestEnum.BA.ToString()); - you will find they both print the same result in each case - without flags it's BA, with flags it's AA. It's not to do with parsing - it maybe the order in which the numeric values are checked to generate a string value. You will also find the TestEnum.BA == TestEnum.AA is true, because numeric values are checked.; – PaulF Jul 04 '17 at 08:30

1 Answers1

12

Firstly, this is nothing to do with Enum.Parse(). The underlying type of an enum by default is int, so in your example TestEnum.AA and TestEnum.BA are both stored as 1 and there is no way to distinguish them.

Witness the following code:

Console.WriteLine(TestEnum.AA); // Prints BA
Console.WriteLine(TestEnum.BA); // Prints BA

Secondly, the reason that setting the [Flags] attribute changes the output is because a different code path is taken when determining the string.

Here's the code from ReferenceSource:

private static String InternalFormat(RuntimeType eT, Object value)
{
    if (!eT.IsDefined(typeof(System.FlagsAttribute), false)) // Not marked with Flags attribute
    {
        // Try to see if its one of the enum values, then we return a String back else the value
        String retval = GetName(eT, value);
        if (retval == null)
            return value.ToString();
        else
            return retval;
    }
    else // These are flags OR'ed together (We treat everything as unsigned types)
    {
        return InternalFlagsFormat(eT, value);

    }
}

Note how GetName() is called if [Flags] is not set, otherwise InternalFlagsFormat() is called.

The implementation of GetName() ends up doing a binary search to find the value, whereas InternalFlagsFormat() winds up doing a linear search to find the value.

InternalFlagsFormat() must do a linear search because it may need to set multiple values (e.g. "X|Y|Z") so Microsoft implemented an O(N) solution for it. However for GetName() they went for a more efficient O(Log2(N)) solution.

A binary search can (and does) find a different duplicate value than the linear search does, hence the difference.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276