Why does unboxing array to an incorrect type not throw an exception in C#?

171 views Asked by At

There is a weird inconsistency with converting and unboxing values and arrays:

var us = new uint[] { 1, 2, 3 };

var i1 = (int)us[0];                 // direct value to value: works
var i2 = (int)(object)us[0];         // unboxing value to value: compiles, throws at runtime
var is1 = (int[])u;                  // direct array to array: does not compile
var is2 = (int[])(object)u;          // unboxing array to array: works!

is2.GetType().Name                   // UInt32[]

Why is array unboxing allowed, when a direct conversion is prohibited?

1

There are 1 answers

2
Sweeper On

Why is array unboxing allowed, when a direct conversion is prohibited?

If you do (int[])(object)us, you are doing two conversions, both of which are allowed by C#:

  • from uint[] to object (you are allowed to cast any reference type to object)
  • from object to int[] (you are allowed to cast from object to any reference type)

Therefore (int[])(object)us compiles.

If you do (int[])us, you are doing a single conversion from int[] to uint[], which is not allowed by C#. According to the C# spec, you can only cast from a T[] to U[] if T and U are reference types, and that you can cast from T to U. int and uint satisfy neither of those, so (int[])us does not compile.

At runtime, the cast from uint[] to int[] is checked to see if it succeeds or fails. The CLR does allow this. See section I.8.7.1 of the CLR spec.

A signature type T is compatible-with a signature type U if and only if at least one of the following holds.

[...]

T is an array with rank r and element type V, and U is an array with the same rank r and element type W, and V is array-element-compatible-with W.

[...]

A signature type T is array-element-compatible-with a signature type U if and only if T has underlying type V and U has underlying type W and either:

  1. V is compatible-with W; or
  2. V and W have the same reduced type.

And the "reduced types" of int and uint are both int.

This is why (int[])(object)us succeeds at runtime.

Also note that casting us to object is not "boxing". Everything is a reference type here. You're just changing the type of the reference.