Here is a code sample:
class Program
{
static void Main(string[] args)
{
var obj = new DerivedClass();
obj.SomeMethod(5);
}
}
class
The precise wording and location varies with different versions of the spec, but for example here one can read:
The set of candidate methods for the method invocation is constructed. Starting with the set of methods associated with M, which were found by a previous member lookup (§7.3), the set is reduced to those methods that are applicable with respect to the argument list A. The set reduction consists of applying the following rules to each method T.N in the set, where T is the type in which the method N is declared:
If N is not applicable with respect to A (§7.4.2.1), then N is removed from the set.
If N is applicable with respect to A (§7.4.2.1), then all methods declared in a base type of T are removed from the set.
So, given that we have obj
of type DerivedClass
then the set of member methods contains void SomeMethod(long)
from DerivedClass
and void SomeMethod(int)
from BaseClass
.
Both of these methods are applicable, and indeed void SomeMethod(int)
is a better overload match, but because of the rule in the last sentence quoted above, once it is found that void SomeMethod(long)
is applicable, all methods from base classes are removed from the set of candidates, meaning that void SomeMethod(int)
is no longer considered.
Okay, that's the technical reason in terms of the spec. What is the design reason behind that being in the spec in the first place?
Well, imagine that BaseClass
started out defined as:
public class BaseClass
{
}
If the rest of the code was the same, then it's pretty obvious that the call to obj.SomeMethod(5)
should call the only so-named method that existed.
Now consider if after that code was written, the method void SomeMethod(int)
was added to BaseClass
. And consider indeed that this could be in a different assembly to DerivedClass
, and by a separate author.
Now the meaning of the call to SomeMethod()
has changed. Worse, it's changed or not depending on which updates a given machine has or hasn't got applied. (And worse again, since return type isn't used in C# overload resolution, it's changed in a way that could produce a compile error in already-compiled code: A full breaking change).
The rule of excluding methods defined in a base class if there are overload candidates from a more derived class allows for greater assurance that one is calling the method one intended to call, in the face of future changes. (Of course you might be surprised if you'd intended the base classes methods to be called, but then at the time of coding you could catch that problem and use a cast to ensure the behaviour you wanted was what resulted).
A consequence of this that can be surprising to some though is in:
class Program
{
static void Main(string[] args)
{
var obj = new DerivedClass();
obj.SomeMethod(5);
}
}
class BaseClass
{
public virtual void SomeMethod(int a) { Console.WriteLine("Base"); }
}
class DerivedClass : BaseClass
{
public override void SomeMethod(int a) { Console.WriteLine("Defined in Base, overriden in Derived"); }
public void SomeMethod(long a) { Console.WriteLine("Derived"); }
}
This outputs Derived
, because this rule applies according to where the method is declared, even if there is an implementation from an override.
(Another reason for the rule working as it does, is that when it's converted into CIL the call will contain information about the class it's declared in. The rule here is the simplest possible way of doing things. That said; 1) A similar logic applied in the design of CIL and 2) the above made this a feature of CIL for the C# people to work with, rather than one to work against).