11

Normally, one would expect, and hope, that two casts are needed to first unbox a value type and then perform some kind of value type conversion into another value type. Here's an example where this holds:

  // create boxed int
  IFormattable box = 42;       // box.GetType() == typeof(int)


  // unbox and narrow
  short x1 = (short)box;       // fails runtime :-)
  short x2 = (short)(int)box;  // OK

  // unbox and make unsigned
  uint y1 = (uint)box;         // fails runtime :-)
  uint y2 = (uint)(int)box;    // OK

  // unbox and widen
  long z1 = (long)box;         // fails runtime :-)
  long z2 = (long)(int)box;    // OK (cast to long could be made implicit)

As you can see from my smileys, I'm happy that these conversions will fail if I use only one cast. After all, it's probably a coding mistake to try to unbox a value type into a different value type in one operation.

(There's nothing special with the IFormattable interface; you could also use the object class if you prefer.)

However, today I realized that this is different with enums (when (and only when) the enums have the same underlying type). Here's an example:

  // create boxed DayOfWeek
  IFormattable box = DayOfWeek.Monday;    // box.GetType() == typeof(DayOfWeek)


  // unbox and convert to other
  // enum type in one cast
  DateTimeKind dtk = (DateTimeKind)box;   // succeeds runtime :-(

  Console.WriteLine(box);  // writes Monday
  Console.WriteLine(dtk);  // writes Utc

I think this behavior is unfortunate. It should really be compulsory to say (DateTimeKind)(DayOfWeek)box. Reading the C# specification, I see no justification of this difference between numeric conversions and enum conversions. It feels like the type safety is lost in this situation.

Do you think this is "unspecified behavior" that could be improved (without spec changes) in a future .NET version? It would be a breaking change.

Also, if the supplier of either of the enum types (either DayOfWeek or DateTimeKind in my example) decides to change the underlying type of one of the enum types from int to something else (could be long, short, ...), then all of a sudden the above one-cast code would stop working, which seems silly.

Of course, the enums DayOfWeek and DateTimeKind are not special. These could be any enum types, including user-defined ones.

Somewhat related: Why does unboxing enums yield odd results? (unboxes an int directly into an enum)

ADDITION:

OK, so many answers and comments have focused on how enums are treated "under the hood". While this is interesting in itself, I want to concentrate more on whether the observed behavior is covered by the C# specification.

Suppose I wrote the type:

struct YellowInteger
{
  public readonly int Value;

  public YellowInteger(int value)
  {
    Value = value;
  }

  // Clearly a yellow integer is completely different
  // from an integer without any particular color,
  // so it is important that this conversion is
  // explicit
  public static explicit operator int(YellowInteger yi)
  {
    return yi.Value;
  }
}

and then said:

object box = new YellowInteger(1);
int x = (int)box;

then, does the C# spec say anything about whether this will succeed at runtime? For all I care, .NET might treat a YellowInteger as just an Int32 with different type metadata (or whatever it's called), but can anyone guarantee that .NET does not "confuse" a YellowInteger and an Int32 when unboxing? So where in the C# spec can I see if (int)box will succeed (calling my explicit operator method)?

Community
  • 1
  • 1
Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • 2
    Enums are essentially labels over their underlying types. There is no unspecified behavior. You are simply changing the labeling of the actual value, not its type. – Panagiotis Kanavos Jul 13 '12 at 14:10
  • 1
    @PanagiotisKanavos According to the C# specification, there is an **explicit** cast from any enum type `E1` to any other enum type `E2`. It does not say that the cast becomes implicit if enum type `E1` and enum type `E2` (are distinct but) happen to have the same underlying type. And surely, with all boxing/unboxing out of the question, you can't assign a `DayOfWeek` to a `DateTimeKind` without making an explicit cast. – Jeppe Stig Nielsen Jul 13 '12 at 14:38
  • @JeppeStigNielsen The C# explicit cast of two enums with the same underlying type results in no casting IL commands in the resulting IL. – Adam Houldsworth Jul 13 '12 at 14:44
  • 1
    @JeppeStigNielsen I've amended my answer with a link to a similar question, the accepted answer I believe covers the root cause. – Adam Houldsworth Jul 13 '12 at 14:52
  • @AdamHouldsworth Yes, that's an interesting answer you link. I still speculate if this implementation is compatible with the C# spec. I tried to call Hans Passant to my thread here :-) Also, I updated my question above with an example where it's a struct type instead of an enum type. – Jeppe Stig Nielsen Jul 13 '12 at 17:58

2 Answers2

5

When you use:

IFormattable box = 42; 
long z2 = (long)(int)box;

You are actually unboxing and then casting.

But in your second case:

IFormattable box = DayOfWeek.Monday; 
DateTimeKind dtk = (DateTimeKind)box;

You don't perform any casting at all. You just unbox the value. The default underlying type of the enumeration elements is int.

Update to refer to the real question:

The specification you mentioned in the comment:

The explicit enumeration conversions are:
...
From any enum-type to any other enum-type.

This is actually correct. We cannot implicitly convert:

//doesn't compile
DateTimeKind dtk = DayOfWeek.Monday;

But we can explicitly convert:

DateTimeKind dtk = (DateTimeKind)DayOfWeek.Monday;

It seems that you found a case when this is still required. But when combined with unboxing, only explicit conversion needs to be specified and unboxing can be ommited.

Update 2

Got a feeling that somebody must have noticed that before, went to Google, searched for "unboxing conversion enum" and guess what? Skeet blogged about it in 2005 (CLI spec mistake with unboxing and enums)

doblak
  • 3,036
  • 1
  • 27
  • 22
  • Have you read the specification "§6.2.2 Explicit enumeration conversions"? Normally, an explicit cast is required to convert between enum types. You can't just assign a `DayOfWeek` to a `DateTimeKind`. An enum type is something different than its underlying integral type. – Jeppe Stig Nielsen Jul 13 '12 at 14:24
  • @JeppeStigNielsen Conversion between enums of the same underlying types actually results in no casting IL instructions. – Adam Houldsworth Jul 13 '12 at 14:39
  • @AdamHouldsworth OK, so that's a description of how it's implemented, and agrees with my observations. But I don't think it agrees with the official C# specification. Nowhere does the spec mention that the explicitness of an enum conversion shall depend on underlying integer types. – Jeppe Stig Nielsen Jul 13 '12 at 14:44
  • @JeppeStigNielsen The C# language is not an exact mirror of the resulting IL, which is why the unboxing action, which isn't controllable in C#, can cheat and unbox to a different *logical* enum of the same underlying type. There are things that can be done in IL, but not in C#. – Adam Houldsworth Jul 13 '12 at 14:45
  • @Darjan Another way of changing the value is `public enum ShortBasedEnum : short`. – Adam Houldsworth Jul 13 '12 at 14:53
  • @AdamHouldsworth Going to the IL level will certainly explain how things are implemented. But actually, that's not what my question is really about. The C# compiler **could** emit some IL that checked for types in case of `(DateTimeKind)box` if it wanted to. The information is there somewhere. But the compiler (and/or the runtime) "forgets" to check type safety in this case. My question is _not_: How is it implemented? Instead my question _is_: Isn't this implementation in disagreement with the C# language specification? – Jeppe Stig Nielsen Jul 13 '12 at 14:54
  • @JeppeStigNielsen Sorry, I took your question title as the question, not the one in the middle of your text. – Adam Houldsworth Jul 13 '12 at 14:56
  • @AdamHouldsworth I can understand your confusion there. Maybe _"In C#, a single cast can perform both an unboxing and an enum conversion, but the specification does not mention that"_ would be a better title (though too long). – Jeppe Stig Nielsen Jul 13 '12 at 15:12
  • @JeppeStigNielsen: The documentation regarding enumeration conversions seems to be just fine. The unboxing is ommited here, you are right about that one. – doblak Jul 13 '12 at 15:31
  • @AdamHouldsworth, Jeppe: see the latest update and Skeet's post, he explained it all. – doblak Jul 13 '12 at 18:21
  • There will probably not come more answers, so I'll accept this one which has become OK after you edited it. I still think it's a shame this happens. Even if the two enums occupy the same number of bits, they're still different types. And it does not happen when unboxing an `int` into a `uint` or a `YellowInteger` into an `int`, although these value types are all just 32 bits of data with different "metadata". – Jeppe Stig Nielsen Jul 19 '12 at 21:48
3

This is because they are actually represented as their underlying value type at runtime. They are both int, which follows the same situation as your failing cases - if you change the enum type in this situation, this would also fail.

As the type is the same, the action is simply unboxing the int.

You can only unbox values to their actual type, hence casting before unboxing does not work.

Update

If you create some code that casts int enumerations around with each other, you'll see that there are no casting actions in the generated IL. When you box an enum and unbox it to another type, there is just the unbox.any action which:

Converts the boxed representation of a type specified in the instruction to its unboxed form.

In this case it is each of the enums, but they are both int.

Update 2:

I've reached my limit of being able to explain what is going on here without much more in-depth research on my part, but I spotted this question:

How is it that an enum derives from System.Enum and is an integer at the same time?

It might go a little ways to explaining how enumerations are handled.

Community
  • 1
  • 1
Adam Houldsworth
  • 63,413
  • 11
  • 150
  • 187
  • 1
    But the enum type is **not** lost when I box an enum. If I have `IFormattable box1 = DayOfWeek.Monday;`, `IFormattable box2 = DateTimeKind.Utc;`, and `IFormattable box3 = (int)1;`, then all these boxes have different `GetType()`, and neither would compare equal under `Object.Equals`. – Jeppe Stig Nielsen Jul 13 '12 at 14:20
  • @JeppeStigNielsen Regardless of the metadata, the underlying value type of the enum is an `int`. There is obviously some extra support for enums in reflection, just like there is support for `Nullable`, which is a `struct`, to contain null values. – Adam Houldsworth Jul 13 '12 at 14:21
  • 1
    But where in the specification does it say: (1) An unboxing conversion and a _numeric_ conversion **cannot** be combined into one cast. (2) An unboxing conversion and an _enumeration_ conversion **can** be combined into one cast? – Jeppe Stig Nielsen Jul 13 '12 at 14:29
  • @JeppeStigNielsen Because you aren't casting, you are swapping something that is an `int` to something that is an `int`. Like I said, if you change one of those enums to be not-`int` and try the same, it would fail for the same reasons. – Adam Houldsworth Jul 13 '12 at 14:30