Why does unboxing enums yield odd results?
Asked Answered
P

3

7

Consider the following::

Object box = 5;
int @int = (int)box;  // int = 5
int? nullableInt = box as int?; // nullableInt = 5;
StringComparison @enum = (StringComparison)box; // enum = OrdinalIgnoreCase
StringComparison? nullableEnum = box as StringComparison?; // nullableEnum = null.

2 things::

  1. Why can I unbox to StringComparison? I guess this is because it's underlying type is Int32 but I still find it odd.
  2. Why does nullableEnum have a value of null?

As I understand the only valid unboxing is from a boxed value type is to it's type or to a nullable type. If int can unbox to Enum, then why doesn't the same hold true for the nullable values? Similarly, if Instead of 5 I boxed StringComparison.OrdinalIgnoreCase, it would be that nullableInt would be null, but nullableEnum would not be.

Plano answered 26/10, 2010 at 17:45 Comment(0)
P
3

Strictly speaking I think it's a bug in implementation detail of the runtime, since the C# spec says

Unboxing to a nullable-type produces the null value of the nullable-type if the source operand is null, or the wrapped result of unboxing the object instance to the underlying type of the nullable-type otherwise.

That is, if unboxing to StringComparison works, then unboxing to Nullable<StringComparison> should work too. It's a little unclear whether both should work or both should fail. The spec says that

For an unboxing conversion to a given non-nullable-value-type to succeed at run-time, the value of the source operand must be a reference to a boxed value of that non-nullable-value-type.

You have to decide whether a boxed int is a considered to be a boxed value of type StringComparison because the underlying type of StringComparison is int. The spec goes on to say that an InvalidCastException is thrown if the box contains an "incompatible object". An int is certainly "compatible" with StringComparison, because you can safely copy the four bytes from the heap into your StringComparison variable.

Priming answered 26/10, 2010 at 18:43 Comment(3)
Re: bug in the runtime: I wouldn't say that it is a bug in the runtime; rather, the runtime is in some cases more lenient than the C# spec requires. It is only at excessive cost that we could restrict the behaviour of the C# implementation to be a strict implementation of the spec, for no real gain.Cooky
Re: "compatible" types: yes, this is a bit of hand waving in the spec. The CLR rules are slightly inconsistent. It is interesting, for instance, that even though int and uint are considered compatible types in the CLR for the purposes of array covariance, they are not considered compatible types in the CLR for the purposes of unboxing, even though int and enum are.Cooky
Fair point, "bug" is a bit harsh. I was reading the first quoted bit as "unboxing to int? should work when unboxing to int works", when it's really "unboxing to int? should work when unboxing to int should work". My point was really that the spec agrees with Michael's intuition and the different results are an "accident" of the runtime's leniency rather than a deliberate design decision that unboxing to nullable enums should be invalid.Priming
G
1

When you cast enum or integer to object, it still holds type information. So box is StringComparison will return false. But it is allowed to cast any enum or int to any enum, so explicit cast (StringComparison)box works. It is a special case for enums. Nullable<T>, on the other hand, is just a usual class, T is not handled in any specific way when you cast or check type. This is why this code will throw exception.

        StringComparison? nullableEnum = (StringComparison?)nullableInt;
Gove answered 26/10, 2010 at 18:2 Comment(0)
H
-1

1) Yes, underlying type of enum is int and that's why it works in this way. Even more. You can do following:

enum MyEnum
{
    One = 1,
    Two = 2,
}

int i = 3;
MyEnum myEnum = (MyEnum)i; // This works without exceptions.

2) Because StringComparison? is actually Nullable<StringComparison> which is different type. And as operator only checks if the object is of the same type as specified in as operator.

Hexapartite answered 26/10, 2010 at 17:49 Comment(1)
Yes, but you can also do long @long = (long)intVar; this doesn't mean you can unbox an int to a long.Plano

© 2022 - 2024 — McMap. All rights reserved.