Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does casting give CS0030, while "as" works?

Suppose I have a generic method:

T Foo(T x) {
    return x;
}

So far so good. But I want to do something special if it's a Hashtable. (I know this is a completely contrived example. Foo() isn't a very exciting method, either. Play along.)

if (typeof(T) == typeof(Hashtable)) {
    var h = ((Hashtable)x);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'
}

Darn. To be fair, though, I can't actually tell if this should be legal C# or not. Well, what if I try doing it a different way?

if (typeof(T) == typeof(Hashtable)) {
    var h = x as Hashtable;  // works (and no, h isn't null)
}

That's a little weird. According to MSDN, expression as Type is (except for evaluating expression twice) the same as expression is type ? (type)expression : (type)null.

What happens if I try to use the equivalent expression from the docs?

if (typeof(T) == typeof(Hashtable)) {
    var h = (x is Hashtable ? (Hashtable)x : (Hashtable)null);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'
}

The only documented difference between casting and as that I see is "the as operator only performs reference conversions and boxing conversions". Maybe I need to tell it I'm using a reference type?

T Foo(T x) where T : class {
    var h = ((Hashtable)x);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'
    return x;
}

What's going on? Why does as work fine, while casting won't even compile? Should the cast work, or should the as not work, or is there some other language difference between casting and as that isn't in these MSDN docs I found?

like image 979
Ken Avatar asked Sep 21 '11 18:09

Ken


2 Answers

Ben's answer basically hits the nail on the head, but to expand on that a bit:

The problem here is that people have a natural expectation that a generic method will do the same thing that the equivalent non-generic method would do if given the types at compile time. In your particular case, people would expect that if T is short, then (int)t should do the right thing -- turn the short into an int. And (double)t should turn the short into a double. And if T is byte, then (int)t should turn the byte into an int, and (double)t should turn the byte into a double... and now perhaps you begin to see the problem. The generic code we'd have to generate would basically have to start the compiler again at runtime and do a full type analysis, and then dynamically generate the code to do the conversion as expected.

That is potentially expensive; we added that feature in C# 4 and if that's what you really want, you can mark the objects as being of type "dynamic" and a little stripped-down version of the compiler will start up again at runtime and do the conversion logic for you.

But that expensive thing is typically not what people want.

The "as" logic is far less complicated than the cast logic because it does not have to deal with any conversions other than boxing, unboxing and reference conversions. It does not have to deal with user-defined conversions, it does not have to deal with fancy representation-changing conversions like "byte to double" that turn one-byte data structures into eight-byte data structures, and so on.

That's why "as" is allowed in generic code but casts are not.

All that said: you are almost certainly doing it wrong. If you have to do a type test in generic code your code is not generic. This is a really bad code smell.

like image 62
Eric Lippert Avatar answered Nov 19 '22 01:11

Eric Lippert


The cast operator in C# can:

  • box/unbox
  • upcast/downcast
  • call a user-defined conversion operator

as Hashtable always means the second.

By eliminating value types with the constraint, you've knocked out option 1, but it's still ambiguous.


Here are the two "best" approaches that both work:

Hashtable h = x as Hashtable;
if (h != null) {
    ...
}

or

if (x is Hashtable) {
    Hashtable h = (Hashtable)(object)x;
    ...
}

The first needs only one type test, so it's very efficient. And the JIT optimizer recognizes the second one, and treats it like the first (at least when dealing with non-generic types, I'm not sure about this particular case.)

like image 8
Ben Voigt Avatar answered Nov 19 '22 00:11

Ben Voigt