I wonder about the following behavior:
public class A {
public virtual void Do() { Console.WriteLine("A"); }
}
public class B : A {
public override void Do() { Console.WriteLine("B override"); }
public void Do(int value = 0) { Console.WriteLine("B overload"); }
}
class Program {
public static void Main() {
new B().Do(); // ---> Console: "B overload"
}
}
I expect that an overload with the exact signature has precedence over another overload with optional parameters: I expect "B override" in the console. Instead the program writes "B overload" into the console.
Even resharper fails and falls into the trap:
... Resharper says that the overload with the optional parameter is hidden by the overload with the exact signature, but in fact it is the contrary.
Now, if you remove the inheritance, then it behaves as expected and resharper warning is legitimate:
public class B {
public virtual void Do() { Console.WriteLine("B override"); }
public void Do(int value = 0) { Console.WriteLine("B overload"); }
}
class Program {
public static void Main() {
new B().Do(); // ---> Console: "B override"
}
}
So the question: What is the precedence rule that explains this observation? Why an overload with exact signature doesn't have precedence over another overload with optional parameters in case that the overload with exact parameters overrides a base implementation?
Why an overload with exact signature doesn't have precedence over another overload with optional parameters in case that the overload with exact parameters overrides a base implementation?
Basically this is the compiler following the rules of the specification, even though they're surprising in this case. (Section 7.6.5.1 is the relevant part of the C# 5 spec.)
The compiler looks at the "deepest" type first - i.e. the one with the compile-time type of the target (B
in this case) and tries to find an applicable function member ignoring any methods which override those declared in a base class:
The set of candidate methods is reduced to contain only methods from the most derived types: For each method C.F in the set, where C is the type in which the method F is declared, all methods declared in a base type of C are removed from the set.
and:
The intuitive effect of the resolution rules described above is as follows: To locate the particular method invoked by a method invocation, start with the type indicated by the method invocation and proceed up the inheritance chain until at least one applicable, accessible, non-override method declaration is found. Then perform type inference and overload resolution on the set of applicable, accessible, non-override methods declared in that type and invoke the method thus selected. If no method was found, try instead to process the invocation as an extension method invocation.
So in your case, the compiler only considers the newly-introduced method, finds it's applicable (using the default parameter value) and stops its search. (i.e. it doesn't look at methods declared in base classes). At that point, the set of candidate function members only has one entry, so there's no real overload resolution to perform at that point.
I have an article on overloading which shows this sort of thing, not using an optional parameter but using a different parameter type - see the "Inheritance" section.
See more on this question at Stackoverflow