Without hesitation, let's just start off with a code sample. When running this program, what output would you expect?
using System;
namespace Alcedo.Demo
{
class Base { }
class Derived : Base { }
class Worker
{
public virtual void Work(Derived param)
{
Console.WriteLine("Worker.Work(Derived)");
}
}
class SpecializedWorker : Worker
{
public override void Work(Derived param)
{
Console.WriteLine("SpecializedWorker.Work(Derived)");
}
public void Work(Base param)
{
Console.WriteLine("SpecializedWorker.Work(Base)");
}
}
class Program
{
public static void Main()
{
Derived param = new Derived();
SpecializedWorker worker = new SpecializedWorker();
worker.Work(param);
}
}
}
Think about it for a while before reading on.
.
.
.
.
.
.
.
That should do. The output is SpecializedWorker.Work(Base). If that was what you
expected (and you know why) you can stop reading here. If not, keep reading for the explanation.
The decision of what method to call can happen in one of two places; either it is done by the
compiler, or it is done by the run-time. Well, the compiler is always involved, but in the
case of virtual methods, it is ultimately the run-time that decides, based on the actual
run-time types of the objects involved. That reasoning seems to indicate that the output of the program
really ought to be SpecializedWorker.Work(Derived). After all, we invoke a method
called Work on an object of the type SpecializedWorker, passing an object
of the type Derived to it. Surely the run-time should have
called SpecializedWorker.Work(Derived)?
It turns out the run-time is not even involved in the decision.
Given the code above, the compiler will make an attempt to decide what method to call. This follows a rather
detailed specification (that you can read here: C# Language Specification, Method invocations).
In brief, it starts off by identifying all possible
candidate methods. A method is a candidate if it:
- Has the same name (in our case
Work)
- It is declared in the type (in our case
SpecializedWorker) or one of its base types
- It is not declared with the
override modifier
At this point there are two candidates to choose from:
public virtual void Worker.Work(Derived)
public void SpecializedWorker.Work(Base)
OK, so at this point SpecializedWorker.Work(Derived param) is not in the candidate
list any longer. In fact calls to override methods should never be; they are called as
a result of overload resolution at run-time, not based on a decision of the compiler. It would still
be called if the compiler decided on using the virtual Worker.Work(Derived) method. But
the compiler does not choose the virtual method. Why?
The answer is found in the Method Invocations document linked above:
If N is applicable with respect to A (Section 7.4.2.1), then all
methods declared in a base type of T are removed from the set.
According to the section linked above, a method is considered to be applicable if the number of
arguments to the method is identical to the argument list in the call, and if there is an implicit
conversion from each argument's type, to the corresponding argument of the method.
Both candidates meet this requirement; both have exactly one argument, and the type Derived can implicitly
be converted to the argument type of each of the two methods. The result of the quoted text from the specification is
that in this case the virtual Worker.Work(Derived) method will be removed from the list of
candidates since it is declared in a base type of SpecializedWorker.
And now the list of candidates is reduced to one single method: SpecializedWorker.Work(Base).
