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:

Unboxing is the act of casting a Reference Type to a Value Type.

We are Unboxing when we do things like:

Unboxing’s Constraints

During Unboxing, two constraints get checked by the CLR:

  1. If the variable containing the reference to the boxed value type instance is null, a NullReferenceException is thrown.
  2. If the reference doesn’t refer to an object that is a boxed instance of the desired value type, an InvalidCastException is thrown.

Jeffrey Richter

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

Tweet
comments powered by Disqus