Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enum.Parse returning unexpected members [duplicate]

Tags:

c#

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?

like image 456
larzz11 Avatar asked Jul 04 '17 08:07

larzz11


1 Answers

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.

like image 87
Matthew Watson Avatar answered Sep 20 '22 20:09

Matthew Watson