This complete C# program illustrates the issue:
public abstract class Executor<T>
{
public abstract void Execute(T item);
}
class StringExecutor : Executor<string>
{
public void Execute(object item)
{
// why does this method call back into itself instead of binding
// to the more specific "string" overload.
this.Execute((string)item);
}
public override void Execute(string item) { }
}
class Program
{
static void Main(string[] args)
{
object item = "value";
new StringExecutor()
// stack overflow
.Execute(item);
}
}
I ran into a StackOverlowException that I traced back to this call pattern where I was trying to forward calls to a more specific overload. To my surprise, the invocation was not selecting the more specific overload however, but calling back into itself. It clearly has something to do with the base type being generic, but I don't understand why it wouldn't select the Execute(string) overload.
Does anyone have any insight into this?
The above code was simplified to show the pattern, the actual structure is a bit more complicated, but the issue is the same.
As other answers have noted, this is by design.
Let's consider a less complicated example:
The question is why
giraffe.Eat(apple)
resolves toGiraffe.Eat(Food)
and not the virtualAnimal.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 aGiraffe
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 ofGiraffe.Eat(Food)
when faced withgiraffe.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:And now all calls to
giraffe.Eat(apple)
insideGiraffe
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:
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/
Looks like this is mentioned in the C# specification 5.0, 7.5.3 Overload Resolution:
When we look at 7.4:
If you remove
override
the compiler picks theExecute(string)
overload when you cast the item.As mentioned in Jon Skeet's article on overloading, when invoking a method in a class that also overrides a method with the same name from a base class, the compiler will always take the in-class method instead of the override, regardless of the "specificness" of type, provided that the signature is "compatible".
Jon goes on to point out that this is an excellent argument for avoiding overloading across inheritance boundaries, since this is exactly the kind of unexpected behavior that can occur.