Is, As, and Casting
C# has two very convenient operators for runtime type checking: is and as.
is takes a class an object and a type and if the object is an instance of the type, is evaluates to true; false otherwise.
as takes an object and a type and if the object is an instance type, as successfully casts the object to the type.
casting blindly takes an object and a type and blindly tries to treat the object as the type. If it fails, it will throw an InvalidCastException.
Now consider the following three methods:
static string AsAsIsString(object o)
{
return o is string ? (string)o : null;
}
static string AsString(object o)
{
return o as string;
}
static string CastString(object o)
{
return (string)o;
}
All of them are more or less equivalent (CastString is the oddball since it could throw). The question is, what happens under the hood? What is the real difference when this code is compiled?
AsAsIsString() is semantically identical to as. It is as implemented in terms of is. The question is whether or not it goes beyond semantic and functional equivalence and is actually identical. Here, I will use the IL for each as the final arbiter:
AsString:
L_0000: ldarg.0
L_0001: isinst string
L_0006: ret
AsAsIsString:
L_0000: ldarg.0
L_0001: isinst string
L_0006: brtrue.s L_000a
L_0008: ldnull
L_0009: ret
L_000a: ldarg.0
L_000b: castclass string
L_0010: ret
CastString:
L_0000: ldarg.0
L_0001: castclass string
L_0006: ret
That’s quite a difference. AsString is pure simplicity – isinst takes the top of the stack and leaves either the object or null if the object is the type. E-Z.
AsAsIsString does much more work. Even with optimization turned on, the compiler doesn’t spot the redundant work. Assuming that castclass and isinst are approximately the same amount of work, this code will take twice as long as AsString.
Finally, we see that the CastString is pretty much the same as AsString.
So the lesson here is that as will be more efficient than the is/cast pair. Although this is a simple case, you might not spot this inefficiency if you wrote code using this pattern:
public void DoSomethingFabulous(object o)
{
if (o is IfNode) {
((IfNode)o).GenerateTestAndBranch();
}
else if (o is WhiteNode) {
((WhileNode)o).GenerateTestAndLoop();
}
else if (o is RepeatNode) {
((RepeatNode)o).GenerateLoopAndTest();
}
}
public void DoSomethingFabulousMoreEfficiently(object o)
{
if ((IfNode ifNode = o as IfNode) != null) {
ifNode.GenerateTestAndBranch();
}
else if ((WhileNode whileNode = o as WhileNode) != null) {
whileNode.GenerateTestAndLoop();
}
else if ((RepeatNode repeatNode = o as RepeatNode) != null) {
repeatNode.GenerateLoopAndTest();
}
}
Ignoring the bad object design here, you can see that the DoSomethingFabulous will have exactly the same problem as AsAsIsString – redundant class checking. DoSomethingFabulousMoreEfficienctly solves that by making as do double duty – it is both a test and an exceptionless cast.
In general, I think it is probably a good idea to expunge is from your code except where doing so would absolutely destroy readability. While this might be considered to be preemptive optimizing, as the performance difference will probably only be noticeable in a large scale usage of this (like say an interpreter for a dynamic language), it’s probably a good idea to keep this in your head – is/cast is 2x costly as as.