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(); try { var x = (Customer)c; } catch (Exception e) { Console.Out.WriteLine(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 .try { 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