Search code examples
c#castingunboxing

C# Type Inference Gets the Wrong Type


I created the following property, which threw an InvalidCastException if the getter was accessed when ViewState[TOTAL_RECORD_COUNT] was null.

public long TotalRecordCount
{
    get { return (long)(ViewState[TOTAL_RECORD_COUNT] ?? -1); }
    set { ViewState[TOTAL_RECORD_COUNT] = value; }
}

My thought is that it incorrectly attempted to unbox the object in ViewState[TOTAL_RECORD_COUNT] to an int, which failed because it contained a long, but I think there might be a flaw in that logic. I will leave it as an exercise to the reader to point out that flaw.

I have since changed that property to read

public long TotalRecordCount
{
    get { return (long?)ViewState[TOTAL_RECORD_COUNT] ?? -1; }
    set { ViewState[TOTAL_RECORD_COUNT] = value; }
}

which works just swell. Still, I am left wondering what was wrong with my original version... StackOverflow to the rescue?

Note that if i try to execute (long)(ViewState[TOTAL_RECORD_COUNT] ?? -1) in the Immediate Window, I get the error message Cannot unbox 'ViewState[TOTAL_RECORD_COUNT] ?? -1' as a 'long' and if I execute (ViewState[TOTAL_RECORD_COUNT] ?? -1).GetType().Name I get Int32. I can execute (long)-1 and end up with -1 as an Int64...so what's up?


Solution

  • The return type of ViewState indexer is Object (I assume you mean ASP.NET viewstate here). Now consider what the compiler has to do when it sees this (which is equivalent to your code):

    object o = ViewState[...];
    var x = o ?? -1;
    

    It has to deduce the result type of expression o ?? -1 somehow. On the left it sees an object, on the right is an int. Clearly, the most general type for this expression is also object. However, this means that if it actually ends up using that -1 (because o was null), it will have to convert it to object - and for an int, this means boxing.

    So x is of type object, and it may contain an int (and maybe also some other integral type - we don't know what is in your viewstate, it could be short, for example). Now you write:

    long y = (long)x;
    

    Since x is object, this is unboxing. However, you can only unbox value types into exact same type (with the only exception being that you can substitute a signed type for an equivalent unsigned type, and enum for its underlying base type). That is, you cannot unbox int into long. A far simpler way to repro this, with no "extra" code, would be:

    object x = 123;
    long y = (long)x;
    

    Which also throws InvalidCastException, and for exact same reason.