What’s the difference between “as” and cast operators?

Most people will tell you that the difference between (Alpha) bravo and bravo as Alpha is that the former throws an exception if the conversion fails, whereas the latter produces null. Though this is correct, and this is the most obvious difference, it’s not the only difference. There are pitfalls to watch out for here.

First off, since the result of the as operator can be null, the resulting type has to be one that includes a null value: either a reference type or a nullable value type. You cannot do x as int, that doesn’t make any sense. If the operand isn’t an int, then what should the expression’s value be? The type of the as expression is always the type named in the expression, so it needs to be a type that can take a null.

Second, the cast operator, as I’ve discussed before, is a strange beast. It means two contradictory things: “check to see if this object really is of this type, throw if it is not” and “this object is not of the given type; find me an equivalent value that belongs to the given type”. The latter meaning of the cast operator is not shared by the as operator. If you say

short s = (short)123;
int? i = s as int?;

then you’re out of luck. The as operator will not make the representation-changing conversions from short to nullable int like the cast operator would. Similarly, if you have class Alpha and unrelated class Bravo, with a user-defined conversion from Bravo to Alpha, then (Alpha) bravo will run the user-defined conversion, but bravo as Alpha will not. The as operator only considers reference, boxing and unboxing conversions.

And finally, of course the use cases of the two operators are superficially similar, but semantically quite different. A cast communicates to the reader “I am certain that this conversion is legal and I am willing to take a runtime exception if I’m wrong”. The as operator communicates “I don’t know if this conversion is legal or not; we’re going to give it a try and see how it goes”.

Notes from 2020

There were a number of pragmatic comments to the original post:

  • There is pressure to use as over casting for legibility reasons; (x as C).M() is perceived as easier to read than ((C)x).M(). Indeed, the cast operator has other syntactic problems than just being ugly; is (C)-1 casting -1 to C, or subtracting 1 from (C)?
  • It seems like there is a missed opportunity for a syntactic sugar that both tests the type and gives you a value of that type in one step; those comments and similar comments from many other users eventually led to if (x as C c) being added to the language.
  • One commenter pointed out that use of type checking operators in generic code is a bad smell; I thoroughly agree.
  • Another pointed out that any sort of type checking at runtime is an indication that the programmer was unable to produce a proof of type safety that could be checked by the compiler, and is therefore suspect. It would be nice if those points were easier to find quickly in a program because they need deeper code review. This is an often neglected aspect of language design; I should write more about that!
  • There was also a long conversation about the use of various operators for different kinds of dynamic dispatch. I discussed these in my Wizards and Warriors series in more detail.

Representation and identity

(Note: not to be confused with Inheritance and Representation.)

I get a fair number of questions about the C# cast operator. The most frequent question I get is:

short sss = 123;
object ooo = sss;            // Box the short.
int iii = (int) sss;         // Perfectly legal.
int jjj = (int) (short) ooo; // Perfectly legal
int kkk = (int) ooo;         // Invalid cast exception?! Why?

Why? Because a boxed T can only be unboxed to T (or Nullable<T>.) Once it is unboxed, it’s just a value that can be cast as usual, so the double cast works just fine.
Continue reading