As other answers have noted, this is by design.
Let's consider a less complicated example:
class Animal
{
public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
The question is why giraffe.Eat(apple)
resolves to Giraffe.Eat(Food)
and not the virtual Animal.Eat(Apple)
.
This is a consequence of two rules:
(1) The type of the receiver is more important than the type of any argument when resolving overloads.
I hope it is clear why this must be the case. The person writing the derived class has strictly more knowledge than the person writing the base class, because the person writing the derived class used the base class, and not vice versa.
The person who wrote Giraffe
said "I have a way for a Giraffe
to eat any food" and that requires special knowledge of the internals of giraffe digestion. That information is not present in the base class implementation, which only knows how to eat apples.
So overload resolution should always prioritize choosing an applicable method of a derived class over choosing a method of a base class, regardless of the betterness of the argument type conversions.
(2) Choosing to override or not override a virtual method is not part of the public surface area of a class. That's a private implementation detail. Therefore no decision must be made when doing overload resolution that would change depending on whether or not a method is overridden.
Overload resolution must never say "I'm going to choose virtual Animal.Eat(Apple)
because it was overridden".
Now, you might well say "OK, suppose I am inside Giraffe when I am making the call." Code inside Giraffe has all the knowledge of private implementation details, right? So it could make the decision to call virtual Animal.Eat(Apple)
instead of Giraffe.Eat(Food)
when faced with giraffe.Eat(apple)
, right? Because it knows that there is an implementation that understands the needs of giraffes that eat apples.
That's a cure worse than the disease. Now we have a situation where identical code has different behaviour depending on where it is run! You can imagine having a call to giraffe.Eat(apple)
outside of the class, refactor it so that it is inside of the class, and suddenly observable behaviour changes!
Or, you might say, hey, I realize that my Giraffe logic is actually sufficiently general to move to a base class, but not to Animal, so I am going to refactor my Giraffe
code to:
class Mammal : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
...
}
And now all calls to giraffe.Eat(apple)
inside Giraffe
suddenly have different overload resolution behaviour after the refactoring? That would be very unexpected!
C# is a pit-of-success language; we want very much to make sure that simple refactorings like changing where in a hierarchy a method is overridden do not cause subtle changes in behaviour.
Summing up:
- Overload resolution prioritizes receivers over other arguments because calling specialized code that knows the internals of the receiver is better than calling more general code that does not.
- Whether and where a method is overridden is not considered during overload resolution; all methods are treated as though they were never overridden for purposes of overload resolution. It's an implementation detail, not part of the surface of the type.
- Overload resolution problems are solved -- modulo accessibility of course! -- the same way no matter where the problem occurs in the code. We do not have one algorithm for resolution where the receiver is of the type of the containing code, and another for when the call is in a different class.
Additional thoughts on related issues can be found here: https://ericlippert.com/2013/12/23/closer-is-better/ and here https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/