4

While using an external API that has several overloads that take different data types but no enums, I decided to create a handy method to give a bit more type-safety to enums and ended up with something like this:

namespace TestEnumPromotion
{
    enum Values
    {
        Value0,
        Value1,
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Prints int
            Overloaded(0);

            // Prints Values
            Overloaded(Values.Value0);

            // Does not compile! :-/
            Overloaded((byte) 0);

            // Prints int
            byte b = 0;
            Overloaded(b);
        }

        static void Overloaded(int i)
        {
            Console.WriteLine("int");
        }

        static void Overloaded(Values i)
        {
            Console.WriteLine("Values");
        }
    }
}

But I was very surprised to see that the code does not compile because Overloaded((byte) 0):

The call is ambiguous between the following methods or properties: 'Program.Overloaded(int)' and 'Program.Overloaded(Values)'

But byte cannot be automatically promoted to Values, that is Values v = (byte)b will not compile because:

Cannot implicitly convert type 'byte' to '`TestEnumPromotion.Values`'.

So the only possible overload should be int, right?

I thought that maybe enums were just syntactic sugar and that the compiler would generate methods that received int, but looking at the IL through ILDASM shows that a method was actually created that takes the enum.

.method private hidebysig static void  Overloaded(valuetype TestEnumPromotion.Values i) cil managed
{
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Values"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
} // end of method Program::Overloaded

What is happening?

Magus
  • 61
  • 5

1 Answers1

4

This is the consequence of a weird little rule in the language: literal 0 is implicitly convertible to any enum.

UPDATE: As per comments and linked sources, the compiler doesn't actually follow the spec. The compiler allows the implicit conversion of any zero constant, not just literal zero. You can read more about this issue here.

This won't compile:

Values someValue = 1; //can not implicitly convert `int` to...

But this, funnily enough, will:

Values someValue = 0;

In your case, because (byte)0 (constant zero) is implicitly convertible to Values and int, the compiler can't choose a best overload and fails unable to resolve the call. If you change your code to (byte)1 or any other literal value, it will compile just fine.

Its worth mentioning that the reason why Overloaded(0) works is simply because the compiler finds an exact match; there is no need of an implicit cast, so Overloaded(int i) wins uncontested.

Quoting the c# 4.0 specs:

1.10 (...) The default value of any enum type is the integral value zero converted to the enum type. In cases where variables are automatically initialized to a default value, this is the value given to variables of enum types. In order for the default value of an enum type to be easily available, the literal 0 implicitly converts to any enum type.(...)

InBetween
  • 32,319
  • 3
  • 50
  • 90
  • 1
    But `(byte) 0` is *not* "the literal `0`", so this is either incomplete or a compiler bug. (I seem to recall there is actual weirdness with constants cast to specific types that the compiler treats more flexibly than it should, but I can't recall the other question where I saw it. It could also be a consequence of the extremely convoluted rules C# has for overload resolution.) – Jeroen Mostert Mar 21 '18 at 20:19
  • 4
    [Found it](https://stackoverflow.com/a/14238286/4137916)! Eric Lippert to the rescue, as usual. The standard is fibbing when it talks about "literal `0`", in fact any *constant* `0` is a candidate for conversion. Mix that with the overload resolution and Bob's your uncle. – Jeroen Mostert Mar 21 '18 at 20:35