59

Is there a logic to which constant I get if there are more than one enum constant that has the same value?

I tried the variations below, but couldn't get a reasonable logic.

Main Method:

public class Program
{
    public static void Main(string[] args)
    {
        Test a = 0;
        Console.WriteLine(a);
    }
}

First try:

enum Test
{
    a1=0,
    a2=0,
    a3=0,
    a4=0,
}

Output:

a2

Second try:

enum Test
{
    a1=0,
    a2=0,
    a3,
    a4=0,
}

Output:

a4

Third try:

enum Test
{
    a1=0,
    a2=0,
    a3,
    a4,
}

Output:

a2

Fourth try:

enum Test
{
    a1=0,
    a2=0,
    a3,
    a4
}

Output:

a1
sertsedat
  • 3,490
  • 1
  • 25
  • 45
  • 22
    Wow, I did not even know that enums could have members with the same value. Kind of erases the point of enums. – xofz May 16 '16 at 22:18
  • 1
    Well, semantically you're right I guess :) But it can also help in some way. Please see http://stackoverflow.com/a/11412641/3729695 – sertsedat May 16 '16 at 22:21
  • 3
    @SamPearson Backward compatibility often requires aliases... – Bakuriu May 17 '16 at 15:07
  • http://stackoverflow.com/questions/9754604/getname-for-enum-with-duplicate-values, http://stackoverflow.com/questions/8043027/non-unique-enum-values, http://stackoverflow.com/questions/26321509/using-an-enum-having-entries-with-the-same-value-of-underlying-type – CodeCaster May 17 '16 at 20:02

2 Answers2

68

The documentation actually addresses this:

If multiple enumeration members have the same underlying value and you attempt to retrieve the string representation of an enumeration member's name based on its underlying value, your code should not make any assumptions about which name the method will return.

(emphasis added)

However, this does not mean that the result is random. What that means it that it's an implementation detail that is subject to change. The implementation could completely change with just a patch, could be different across compilers (MONO, Roslyn, etc.), and be different on different platforms.

If your system is designed that it requires that the reverse-lookup for enums is consistent over time and platforms, then don't use Enum.ToString. Either change your design so it's not dependent on that detail, or write your own method that will be consistent.

So you should not write code that depends on that implementation, or you are taking a risk that it will change without your knowledge in a future release.

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • @jackjop Maybe put it into a loop to iterate it about 1000 times and put the values into a list and see if it's always `a1`. I suspect that wouldn't be the case and is just randomized by how the Enum is compiled. – Drew Kennedy May 16 '16 at 21:10
  • 5
    Not so much logic as 'just how the compiler is currently implemented'. MSDN is telling you that any logic you infer is not to be trusted or relied on. – Steve Cooper May 16 '16 at 21:11
  • 30
    Some behaviors will give the appearance of being deterministic. They might behave exactly the same way 99.9% of the time. They might even *be* deterministic and behave the same way 100% of the time. But it's not guaranteed to always be predictable. It's best not to rely on undocumented behaviors, especially when there's an explicit warning regarding a behavior. – Scott Hannen May 16 '16 at 21:17
  • Could anyone please link the official page where this was noted? Thanks – Paras May 16 '16 at 21:19
  • Nevermind, I've found it: https://msdn.microsoft.com/en-us/library/a0h36syw%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 – Paras May 16 '16 at 21:25
  • If you wanted to explore, you could use ildasm.exe. That might show you why certain values were preferred in any one program -- say, the first one declared in the IL, or similar. Expect that to change routinely as development continues, though. – Steve Cooper May 16 '16 at 21:31
  • @jackjop See my additions. Nothing says that it's _random_; just that it's an implementation detail that you shouldn't code against. It could change in a future release with no notice. – D Stanley May 16 '16 at 22:02
  • 2
    @ParasDPain I linked to it in the answer. – D Stanley May 16 '16 at 22:03
  • 2
    @jackjop - Depending on the implementation, it's also theoretically possible that it would change with each compile. Each build would always retrieve the same value, but the next build would be different. No idea what kind of implementation would do that, but it's allowed by the spec. – Bobson May 17 '16 at 00:18
  • @jackjop: "*So are you saying that that can be changed with each compile?*" Maybe (probably) not every compile; but it **is** subject to change every release (major, minor, update, servicepack, whatever) of the .Net framework. Also note that Mono, for instance, may have a different implementation. Long story short: just **do not rely on this behaviour** as it's **explicitly** documented as such (to be subject to change). And yes, it **is** entirely possible it changes every compile (or run) if the implementation decides to pick a random one instead of the current behaviour (see Bobsons reply). – RobIII May 17 '16 at 08:52
  • 1
    @jackjop: "*So there has to be a logic behind this.*". There always is; it's computers. That doesn't mean that the logic cannot change between runs, compiles, different platforms, updates etc. etc. The documentation states "**your code should not make any assumptions about which name the method will return**". If you decide to ignore that and go from your reverse-engineered logic that don't come crying when Microsoft (or whomever) decides to change the implementation. It's an internal implementation detail; **never** rely on **internal** implementation details, especially if documented a such. – RobIII May 17 '16 at 09:01
  • 3
    @jackjop more likely in six years time the new compiler changes what it "always" does and now your code is riddled with bugs because you ignored the warning. – OrangeDog May 17 '16 at 10:49
28

TL;DR: All fields of an enum will be extract by reflection, then insertion sorted and binary searched for the first matching value.


The call chain looked this :

Enum.Tostring();
Enum.InternalFormat(RuntimeType eT, Object value);
Enum.GetName(Type enumType, Object value);
Type.GetEnumName(object value);

Type.GetEnumName(object value) is implemented as such :

    public virtual string GetEnumName(object value)
    {
        // standard argument guards...

        Array values = GetEnumRawConstantValues();
        int index = BinarySearch(values, value);

        if (index >= 0)
        {
            string[] names = GetEnumNames();
            return names[index];
        }

        return null;
    }

Both GetEnumRawConstantValues() and GetEnumNames() rely on GetEnumData(out string[] enumNames, out Array enumValues) :

    private void GetEnumData(out string[] enumNames, out Array enumValues)
    {
        Contract.Ensures(Contract.ValueAtReturn<String[]>(out enumNames) != null);
        Contract.Ensures(Contract.ValueAtReturn<Array>(out enumValues) != null);

        FieldInfo[] flds = GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);

        object[] values = new object[flds.Length];
        string[] names = new string[flds.Length];

        for (int i = 0; i < flds.Length; i++)
        {
            names[i] = flds[i].Name;
            values[i] = flds[i].GetRawConstantValue();
        }

        // Insertion Sort these values in ascending order.
        // We use this O(n^2) algorithm, but it turns out that most of the time the elements are already in sorted order and
        // the common case performance will be faster than quick sorting this.
        IComparer comparer = Comparer.Default;
        for (int i = 1; i < values.Length; i++)
        {
            int j = i;
            string tempStr = names[i];
            object val = values[i];
            bool exchanged = false;

            // Since the elements are sorted we only need to do one comparision, we keep the check for j inside the loop.
            while (comparer.Compare(values[j - 1], val) > 0)
            {
                names[j] = names[j - 1];
                values[j] = values[j - 1];
                j--;
                exchanged = true;
                if (j == 0)
                    break;
            }

            if (exchanged)
            {
                names[j] = tempStr;
                values[j] = val;
            }
        }

        enumNames = names;
        enumValues = values;
    }

When followed, GetFields(BindingFlags bindingAttr) leads to an abstract method, but searching "GetFields" on msdn will yield you EnumBuilder.GetFields(BindingFlags bindingAttr). And if we follow its call chain :

EnumBuilder.GetFields(BindingFlags bindingAttr);
TypeBuilder.GetFields(BindingFlags bindingAttr);
RuntimeType.GetFields(BindingFlags bindingAttr);
RuntimeType.GetFieldCandidates(String name, BindingFlags bindingAttr, bool allowPrefixLookup);
RuntimeTypeCache.GetFieldList(MemberListType listType, string name);
RuntimeTypeCache.GetMemberList<RuntimeFieldInfo>(ref MemberInfoCache<T> m_cache, MemberListType listType, string name, CacheType cacheType);
MemberInfoCache<RuntimeFieldInfo>.GetMemberList(MemberListType listType, string name, CacheType cacheType);
MemberInfoCache<RuntimeFieldInfo>.Populate(string name, MemberListType listType, CacheType cacheType);
MemberInfoCache<RuntimeFieldInfo>.GetListByName(char* pName, int cNameLen, byte* pUtf8Name, int cUtf8Name, MemberListType listType, CacheType cacheType);
MemberInfoCache<RuntimeFieldInfo>.PopulateFields(Filter filter);
// and from here, it is a wild ride...

So, I will quote Type.GetFields remarks :

The GetFields method does not return fields in a particular order, such as alphabetical or declaration order. Your code must not depend on the order in which fields are returned, because that order varies.

Xiaoy312
  • 14,292
  • 1
  • 32
  • 44
  • 14
    if all else fails read the manual. If that fails too - read the source – pm100 May 16 '16 at 21:39
  • 2
    interesting to see how expensive this is at run time. I had assumed that the compiler would work it out at compile time, apparently not – pm100 May 16 '16 at 21:42
  • 16
    That's how it;s _currently_ implemented. There's nothing that guarantees that it;'s implemented the same way in all versions, on all platforms, or by all compilers. – D Stanley May 16 '16 at 22:03
  • 2
    When Oracle database version 10 came out, they did some internal optimizations where SQL queries that did not have an "order by" sorting clause but did have a group by clause, no longer were always sorted by the group by columns. Up to version 9, it would also sort by the group by, but that was never guaranteed, and a lot of code blew up when upgrading to Oracle 10. I know some of mine did! – Mark Stewart May 17 '16 at 02:34