For the last time, you can't unbox a value type as a diff---wait, what's this?

The Philosopher Developer

January 07, 2011

Remember when I wrote that post half a year ago about how converting and unboxing aren't the same thing, and you can only unbox value types as their original types, yada yada yada?

Yeah, well. Not 100% true. Nullable value types get boxed as their non-nullable counterparts, which means there is no way to "unbox" a nullable value; or in other words, boxing a nullable might be considered a form of lossy compression.

Now, until recently I believed that this was the only exception to the abovementioned rule. That is until I stumbled upon this oddity:

enum Animal { Dog, Cat }

public class Program
{
    public static void Main(string[] args)
    {
        // Box it.
        object animal = Animal.Dog;

        // Unbox it. How are these both successful?
        int i = (int)animal;
        Enum e = (Enum)animal;

        // Prints "0".
        Console.WriteLine(i);

        // Prints "Dog".
        Console.WriteLine(e);
    }
}

What is going on there? How did I manage to unbox animal as both an int and an Animal?

In case you aren't sharing my confusion, allow me to remind you of a few key facts:

  1. int (a.k.a. System.Int32) is a value type. Value types are sealed by default in .NET.
  2. All enum types derive from System.Enum.
  3. C# does not support multiple inheritance.

With these things in mind, I will state my question again: What is going on above? This behavior was so puzzling to me that I asked about it on Stack Overflow.

The answers I got were interesting indeed. It took me a while to get my head around all of the information provided; but I think I've got it now. And just so you can experience my confusion and get a taste of the weirdness of all this for yourself, allow me to share with you the major turning points in what was, for me, a perplexing mental trajectory.

Probably the first useful answer came from a user called Damien_The_Unbeliever. He pointed me to Partition II of the ECMA-335 standard, which includes the following text about enums:

Enums obey additional restrictions beyond those on other value types. Enums shall contain only fields as members (they shall not even define type initializers or instance constructors); they shall not implement any interfaces; they shall have auto field layout; they shall have exactly one instance field and it shall be of the underlying type of the enum; all other fields shall be static and literal; and they shall not be initialized with the initobj instruction.

OK, this is certainly helpful. The above seems to contribute to an overall picture of how any enum type works. It clearly is its own type, with a single instance field of an "underlying type" (generally/by default int) but not synonymous with said type.

But then came along another answer from a user named yodaj007, who broke down the CIL generated by assigning an enum value to an int variable and showed no cast:

.locals init (
    [0] valuetype ConsoleTesting.Foo x)
L_0000: nop 
L_0001: ldc.i4.5  // push 5 onto the local stack
L_0002: stloc.0   // pop 5 off the stack... directly into x (Foo)!

What's this? No cast at all, huh? I guess that's legal because the bits in an int are the same as those in an enum value.

But... it's still got a type, right? What about the fact that Enum.ToString doesn't work the same as int.ToString, for example? It seems that unboxing an object as either an int or an enum value must surely depend on the type of object that has been boxed, right? Am I going crazy?

No... it turns out there is a logical explanation for all this after all (phew!). The best answer ended up coming from Hans Passant, who explained what's really happening: the JIT compiler does not unbox enum values the same way it unboxes other value types:

All [the JIT compiler] has to do [when unboxing normal values] is to check the method table pointer in the object and verify that it is the expected type. And copy the value out of the object directly. [...] The problem with Enum types is that this cannot work. Enums can have a different GetUnderlyingType() type. In other words, the unboxed value has different sizes so simply copying the value out of the boxed object cannot work. Keenly aware, the jitter doesn't inline the unboxing code anymore, it generates a call to a helper function in the CLR.

This explains the discrepancy between Damien_The_Unbeliever's answer and yodaj007's. Yes, enum types are distinct from their underlying types (otherwise, System.Enum.ToString wouldn't behave any differently from int.ToString... not to mention that an enum type couldn't derive from System.Enum in the first place); at the same time, yes they are also bitwise equivalent to their underlying types and for this reason an enum value "freely interconverts with its underlying type" (to borrow a bit more language from ECMA-335).

I will henceforth refer to this phenomenon as:

Enum-Integer Duality

The unboxing mystery—what had been the "missing link" in my mind—is solved by understanding how the JIT compiler gives special treatment to enum values. So there you have it.

So, you might say that Nullable<T> and Enum are both upperclass citizens of the CLR world. They don't live by the same rules as the rest of the lowly value types.

Did you enjoy reading this? Buy Me A Coffee