Throwing an Invalid Cast Exception?... Look Out for Unboxing
Implicit and explicit numeric casts are usually pretty straightforward:
int i = 5;
float f = i; // Implicit cast from Int32 to Single
Byte b = (Byte) i; // Explicit cast from Int32 to Byte
If you’re not paying attention, a safe explicit numeric cast can throw a runtime exception:
Object i = 5;
float l = (float) i; // System.InvalidCastException: Specified cast is not valid.
ArrayList a = new ArrayList();
int b;
for (int i = 0; i < 10; i++) {
b = 10;
a.Add(b);
}
float f = (float) a[0]; // System.InvalidCastException: Specified cast is not valid.
Why is a cast from int
to float
ok, but a cast from int ArrayList[i]
to float
not ok?
Type Casting vs Numeric Casting
Our InvalidCastException
is not coming from our Explicit Cast from int
to float
.
Our exception is coming from our Unboxing an int
to float
.
A key difference between Value Types and Reference Types is where they get allocated. Value Types are allocated in the current Thread Stack. Reference Types are allocated in the Managed Heap (think garbage collection).
Boxing is the act of casting a Value Type to a Reference Type.
We are Boxing when we do things like:
- assign value types to reference type fields
- add value types to a collection reference type
- pass a value type as a parameter to a method that takes a reference type
Unboxing is the act of casting a Reference Type to a Value Type.
We are Unboxing when we do things like:
- extract a value type from a reference type
- extract a value type from a collection of value types
- pass a boxed value type to a method that takes a value type
Unboxing’s Constraints
During Unboxing, two constraints get checked by the CLR:
- If the variable containing the reference to the boxed value type instance is
null
, aNullReferenceException
is thrown.- If the reference doesn’t refer to an object that is a boxed instance of the desired value type, an
InvalidCastException
is thrown.
Unboxing to a Value Type that’s different from the originally Boxed Value Type is not allowed.
Object i = 5; // Boxing int 5 into reference type Object
float l = (float)i; // Trying to unbox int b to float
ArrayList a = new ArrayList();
int b;
for (int i = 0; i < 10; i++) {
b = 10;
a.Add(b); // Boxing int b into reference type ArrayList
}
float f = (float) a[0]; // Trying to unbox int b to float
So now that we understand the problem, what’s the solution?
System.Convert Namespace
If you look at the .NET Reference Source for System.Int32
, you’ll notice that it implements IConvertible
.
public struct Int32 :
IComparable,
IFormattable,
IConvertible,
IComparable<Int32>,
IEquatable<Int32>
Value Types like System.Int32
that implement IConvertible
use the static System.Convert
to do our Unboxing and Explicit Casting.
Here’s the definition of System.Convert.ToSingle
from the .NET Reference Source:
public static float ToSingle(object value) {
return value == null? 0: ((IConvertible)value).ToSingle(null);
}
If we pass a Boxed int
into .ToSingle(object value)
, the method calls the internal .ToSingle()
method defined by the System.Int32
struct:
/// <internalonly/>
float IConvertible.ToSingle(IFormatProvider provider) {
return Convert.ToSingle(m_value);
}
Our corrected examples:
Object o = 5;
float f = Convert.ToSingle(o);
ArrayList a = new ArrayList();
int b;
for (int i = 0; i < 10; i++) {
b = 10;
a.Add(b);
}
Convert.ToSingle(a[0]);
There’s more sample code here if you want to check it out.
Summary
We ran into a runtime exception when trying to do an explicit numeric cast that is usually successful.
The real cause of the issue is the way that we were casting from a Value Type to a Reference Type and then back to a Value Type. This is called Boxing and Unboxing.
One of the rules of Unboxing is that we can only Unbox into the same Value Type we started with. Otherwise, we throw an InvalidCastException
at runtime.
Our solution is to take advantage of the System.Convert
static class. It provides us with a .ToXXX
method that can be called for Value Types that implement IConvertible
.
Hope this finds its way to you if/when you run into this problem, or it helps you stay aware of what the CLR is doing behind the scenes. As always, feel free to reach out if you need any help, sign up for my newsletter, and/or leave a comment below!
Sources
- Richter, Jeffrey. CLR via C#, 4th Edition. Microsoft Press, 2012. ISBN 9780735667457
- .NET Reference Source, .NET Framework 4.6.1. system\convert
- .NET Reference Source, .NET Framework 4.6.1. system\int32