C-Sharp Casting of Types

This post is a response to an article from a series of posts from Iris Classon called “Stupid Questions”, and this particular article discusses the preferred way to cast types in c-sharp.  


So… how should we cast a type in c-sharp?  The ‘as’ keyword gives us the capability of casting reference types without an error.  In other words, if you are attempting to cast to a type that is NOT supported by the type submitted, no error is thrown.  This is a VERY good thing.

For reference, if you are casting a value type (int, long, double, decimal) you don’t have the choice of using the “as” statement.  Instead, you will need to use a Convert.ToInt32() statement (or something similar) to change types.  However, this discussion isn’t about value types…

Consider: If you want to ensure that an object is cast properly to another type, you will need to wrap your direct cast statement in a try-catch block to ensure that no error is thrown.  Your statement will look similar to the following:

            var c = new object();

                var x = (Customer)c;
            catch (Exception e)

This seems like a very simple statement, and an acceptable way to handle the scenaro.  The same operation with an “as” clause will look like:

            var c = new object();

            var x = c as Customer;
            if (x == null)
                Console.Out.WriteLine("Did not cast properly");

But lets look a little closer at the IL (Interpreted Language) generated by the compiler by these two examples.  First, the direct cast:

.locals init ([0] object c,
           [1] class CastDemo.Customer x,
           [2] class [mscorlib]System.Exception e)
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0006:  stloc.0
    IL_0007:  nop
    IL_0008:  ldloc.0
    IL_0009:  castclass  CastDemo.Customer
    IL_000e:  stloc.1
    IL_000f:  nop
    IL_0010:  leave.s    IL_0023
  }  // end .try
  catch [mscorlib]System.Exception 
    IL_0012:  stloc.2
    IL_0013:  nop
    IL_0014:  call       class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
    IL_0019:  ldloc.2
    IL_001a:  callvirt   instance void [mscorlib]System.IO.TextWriter::WriteLine(object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_0023
  }  // end handler
  IL_0023:  nop
  IL_0024:  ret

You can clearly see that the Try statement is ALWAYS fired in the first example… we will always run through the test for the exception.  In fact, the exception object is already allocated in memory on the first line!  Additionally, the call to castclass on line IL_0009 is where we attempt to make our conversion.. if no error is thrown, it jumps to line IL_0023.

… and then the IL for the “as” statement example:

  .locals init ([0] object c,
           [1] class CastDemo.Customer x,
           [2] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  isinst     CastDemo.Customer
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldnull
  IL_0010:  ceq
  IL_0012:  ldc.i4.0
  IL_0013:  ceq
  IL_0015:  stloc.2
  IL_0016:  ldloc.2
  IL_0017:  brtrue.s   IL_0029
  IL_0019:  call       class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
  IL_001e:  ldstr      "Did not cast properly"
  IL_0023:  callvirt   instance void [mscorlib]System.IO.TextWriter::WriteLine(string)
  IL_0028:  nop
  IL_0029:  ret

This snippet shows that the code on line IL_0008 checks for the instance type and on line IL_0017 will exit the method if the variable is set to null.  The difference in our safety check is that the second example is directly asking if the type was converted properly, instead of relying on the try-catch to rescue us when the code doesn’t behave properly.

The performance cost of the try-catch block is trivial, a few microseconds on each call.. but the practice that is troubling here is the use of try-catch to control flow.

In computer science, we define “control flow” as the order in which coded statements are executed.  We can control flow with constructs like if, switch, and for.  If you suspect that your try-catch wrapping the cast statement is going to fail ‘regularly’ and that the catch will not truly be an ‘exceptional’ case… then you are using the try-catch to control flow.  This is considered an anti-pattern, as it changes the intent of the catch clause.

Consider:  if you have coded a catch block that traps the abstract Exception object, you have now hidden EVERYTHING that could possibly go wrong with your cast statement.  There are plenty of reasons why your cast failed, and the exception that the runtime will throw when your cast fails is System.InvalidCastException  If you need to go this route, then catch this exception, because otherwise you are hiding all kinds of events that COULD be happening to your application.

Either of these techniques is valid for casting variables.    I hope this gives some insight into what the CLR is doing when you cast types, and also demonstrate good practices when converting reference types in c-sharp