views:

257

answers:

5

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?

A: 

Int64 is a value type, so casting null to a value type will always throw an exception (NullReferenceException). And casting an Int32 to Int64 will succeed and will not throw an InvalidCastException.

Darin Dimitrov
Unless I'm missing something he's not trying to cast null though? He has the null coalescing operator on the end.
Steven Robbins
i think you need to read the code a bit more closely...the actual cast that is failing is attempting to cast -1 to an Int64
Chris Shouts
+1  A: 

In your original, if you break it down, you were doing:

(ViewState[TOTAL_RECORD_COUNT] ?? -1)

The null-coalescing operator (??) is specifially designed to:

to define a default value for a nullable value types as well as reference types.

In your case, you're using it to handle a System.Object, so it's going to take your "-1", treat it as an Int32, and box it into a new System.Object. Then, it tries to unbox the Int32 into a long, which fails, since the cast cannot unbox and change the type in a single step.

You can solve this easily by specifying that your -1 is a long by using the L suffix:

public long TotalRecordCount
{
    get { return (long)(ViewState[TOTAL_RECORD_COUNT] ?? -1L); }
    set { ViewState[TOTAL_RECORD_COUNT] = value; }
}
Reed Copsey
That's not the problem. Using ?? with a value type would be a compile-type error.
Daniel
Using `??` with a non-nullable value type on the left side would cause a compile-time error though. Also, the return type of `ViewState.this[]` is `object` which is obviously a reference type.
Pavel Minaev
+1  A: 

The problems is not the unboxing of the ViewState[TOTAL_RECORD_COUNT], the problem is the boxing and unboxing of the -1.

   ViewState[TOTAL_RECORD_COUNT] ?? -1

You are using the ?? operator on "object" and "int". The resulting type is "object". This means the -1 will be boxed (as int) when the field does not exist in the view state.

Then your program crashes later when it tries to unbox the (int)-1 as a long.

Daniel
+4  A: 

A cast has to be one step only.

The expression <object> ?? <int> will produce another object, and when the first value is null, ie. ViewState[TOTAL_RECORD_COUNT] is null, then the resulting value will be an object, with a boxed Int32 in it.

Since you cannot unbox an object containing an Int32 to a long, you need to first unbox it to an Int32, and then cast it to a long.

Lasse V. Karlsen
+10  A: 

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.

Pavel Minaev
Thank you for the clear and complete explanation. I will sleep much better tonight!
Chris Shouts