Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Boxed nullable underlying type can be cast to enum but boxed enum type can't be cast to nullable type

  • Boxed nullable underlying type can be cast to enum but boxed enum type can't be cast to nullable type.

And similarly,

  • Boxed nullable enum can be cast to underlying type but boxed underlying type can't be cast to nullable enum.

Ok, I know "boxed nullable type" is not the best way to describe it, but it's for the sake of the question. I'm aware it's the underlying value type that's getting boxed.

I will show it with examples. Assume I have an enum with int as the underlying type.

enum Sex { Male, Female }

Case I:

int? i = 1;
object o = i;
Sex e = (Sex)o; //success

//but

Sex e = Sex.Male;
object o = e;
int? i = (int?)o; //invalid cast

Case II:

Sex? e = Sex.Male;
object o = e;
int i = (int)o; //success

//but

int i = 1;
object o = i;
Sex? e = (Sex?)o; //invalid cast

In a nutshell,

(enum)int? -> succeeds
(int?)enum -> the reverse fails

(int)enum? -> succeeds
(enum?)int -> the reverse fails

Or in even simpler terms,

cast to non-nullable -> succeeds
cast to nullable -> fails

Now I do know that once you box a value type, it can be cast back only to the original type. But since by C# rules, a boxed int can be cast to enum and a boxed enum can be cast to int, and a boxed int to int? and a boxed int? to int, I was looking for a consistent understanding of other scenarios as well, ie the ones listed above. But I dont get the logic. For one, I feel if they all failed or if they all succeeded, it made more sense to developers. Two, even the successful casts look a little odd. I mean since a value type can be implicitly cast to its nullable equivalent (and not the other way around), a cast to nullable should anyway succeed, but with the current implementation a nullable type is being successfully cast to non-nullable which can even fail if the former had a null value. Had this whole thing been other way around, it would have been easier comprehending. An example like:

Sex? e = null;
object o = e;
int i = (int)o; //succeeds, but sure to explode on cast

//but

int i = 1;
object o = i;
Sex? e = (Sex?)o; //invalid cast, even though its always a safe cast

Questions:

  1. So what C# rule is letting this happen?

  2. Is there a simple way I can remember this?

like image 533
nawfal Avatar asked Oct 03 '22 02:10

nawfal


1 Answers

I think this is a subtlety of the unbox and unbox.any IL instructions.

From ECMA 335, section III.4.32 (unbox operation - unbox.any is similar)

Exceptions:
System.InvalidCastException is thrown if obj is not a boxed value type, valuetype is a Nullable<T> and obj is not a boxed T, or if the type of the value contained in obj is not verifier-assignable-to (III.1.8.2.3) valuetype.

So for example, in this case:

Sex e = Sex.Male;
object o = e;
int? i = (int?)o;

it fails entirely correctly - because valuetype is Nullable<int> and the value of obj is not a boxed int. The "verifier-assignable-to" part doesn't apply for the Nullable<T> case.

I doubt that any of this behaviour is described in the C# specification, unfortunately - I don't think the unboxing behaviour from "boxed int" to "enum with an underlying type of int" is described, as far as I can see, which is a sort of prerequisite to then including nullability in the mix.

like image 152
Jon Skeet Avatar answered Oct 11 '22 00:10

Jon Skeet