In C#, why can a single cast perform both an unboxing and an enum conversion?
Asked Answered
C

2

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)?

Clearcut answered 13/7, 2012 at 14:4 Comment(5)
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.Occult
@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.Clearcut
@JeppeStigNielsen The C# explicit cast of two enums with the same underlying type results in no casting IL commands in the resulting IL.Smackdab
@JeppeStigNielsen I've amended my answer with a link to a similar question, the accepted answer I believe covers the root cause.Smackdab
@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.Clearcut
D
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)

Diablerie answered 13/7, 2012 at 14:9 Comment(11)
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.Clearcut
@JeppeStigNielsen Conversion between enums of the same underlying types actually results in no casting IL instructions.Smackdab
@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.Clearcut
@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#.Smackdab
@Darjan Another way of changing the value is public enum ShortBasedEnum : short.Smackdab
@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?Clearcut
@JeppeStigNielsen Sorry, I took your question title as the question, not the one in the middle of your text.Smackdab
@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).Clearcut
@JeppeStigNielsen: The documentation regarding enumeration conversions seems to be just fine. The unboxing is ommited here, you are right about that one.Diablerie
@AdamHouldsworth, Jeppe: see the latest update and Skeet's post, he explained it all.Diablerie
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".Clearcut
S
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.

Smackdab answered 13/7, 2012 at 14:9 Comment(4)
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.Clearcut
@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<T>, which is a struct, to contain null values.Smackdab
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?Clearcut
@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.Smackdab

© 2022 - 2024 — McMap. All rights reserved.