Working with object oriented programs mostly is very straightforward. But sometimes the execution just looks like a bit of magic. Especially the virtual methods where the compiler did know about the static type at the compile time BUT that virtual method was called at the runtime. How does it work? I will try to explain that in this post.
First of all we have to understand how does our execution look like from the intermediate language point of view.
There are two IL instructions for calling methods – call and callvirt. The assumption you make based on their names is correct – one is used for calling static methods with, the second for virtual ones (REMEMBER: static methods can NEVER be virtual – yet you have to specify the type name to call it!). One thing worth mentioning is that the CLR virtual dispatch works at the method level – i.e. properties will be considered as two methods (get_X and set_X).
The motto you should remember about virtual methods is rather simple – the least derived type declaring the virtual method will be used. What does it mean is – the runtime will call to the most specific implementation of the method it can find.
Learn by example
I know this could be quite confusing – and learning by reading is not always the best way, and the easiest way for sure. Let’s consider a simple hierarchy of classes:
public virtual void Do()
class BClass : AClass
public override void Do()
class CClass : AClass
public void Do()
The structure is rather simple, so let’s try to use such implementation in sample code:
AClass a = new AClass();
AClass b = new BClass();
AClass c = new CClass();
The question rises – will you be able to answer that the output will look like that on the prtsc below?
As you can see there are two interesting cases (let’s leave the a object, this is just way too simple! :_) ) – b and c objects. Each of them has it’s own story, let’s talk about it now.
b and c objects and the override keyword
This is cool – you have AClass object, but you assign to it an object which type is BClass. This means that the runtime has the info about it. Let’s dive into the IL code generated:
IL_0001: newobj instance void TestVirtualityApp.AClass::.ctor()
IL_0007: newobj instance void TestVirtualityApp.BClass::.ctor()
IL_000d: newobj instance void TestVirtualityApp.CClass::.ctor()
IL_0014: callvirt instance void TestVirtualityApp.AClass::Do()
IL_001b: callvirt instance void TestVirtualityApp.AClass::Do()
IL_0022: callvirt instance void TestVirtualityApp.AClass::Do()
As you can expect there are two interesting lines for our derived objects – line IL_001b and IL_0022. As you can see both of them use callvirt method call to execute, AClass::Do() method. That is what you would expect from the compiler – the type of the object is AClass so we need to call its method. But the reason we get such results is of course the IL instruction.
As you can read on the spec of the language, this instruction is used for late-bound method call on an object. This is where the whole “magic” lies – late-bound call found that BClass overrides the original virtual method Do() from AClass (because of the override keyword). This is called the polymorphism, where for the base class object the most specific type is called.
But why doesn’t the c object method Do() been called? Everything will be clear if you look at the IL_0022 line where the call is made. We call AClass method and there is no direct connection between Do() in CClass and Do() in AClass. Even though the name of the method is the same, we are calling the Do() method on the AClass and the runtime has no connection between this method and the method in CClass. This would be the case if there was the override keyword.
The new operator – not only for object creation
The designers of the language found this mechanism quite confusing for most developers and introduced the new keyword in the context of overriding methods. This one mustn’t be confused with the ctor calls (how the compiler achieves such differential is the other topic).
One will get the warning CS0114 message from the compiler when he will write a method with the same signature like the base class and there will be no override for the virtual method (this warning states that if the inherited member is hidden there will be a warning). This will make the code more readable – there will be no confusion whether this is another method or the overridden one.
EDIT: One interesting thing to be noticed in here – the different approach between Rebuild and Build. During the first one – there will be a warning because the compiler will do a full build of the project. But the second one will not find any change (if none was made) so no warning message! Quite confusing…
The easiest way to deal with such cases is just to play with the code – you can start with the one I posted and extend it by more classes and dependencies.. I hope this background makes it a good starting point for you to explore this very interesting topic.