Overload resolution and virtual methods
Asked Answered
T

5

23

Consider the following code (it's a little long, but hopefully you can follow):

class A
{
}

class B : A
{
}

class C
{
    public virtual void Foo(B b)
    {
        Console.WriteLine("base.Foo(B)");
    }
}

class D: C
{
    public override void Foo(B b)
    {
        Console.WriteLine("Foo(B)");
    }

    public void Foo(A a)
    {
        Console.WriteLine("Foo(A)");
    }
}

class Program
{
    public static void Main()
    {
        B b = new B();
        D d = new D ();
        d.Foo(b);
    }
}

If you think the output of this program is "Foo(B)" then you'd be in the same boat as me: completely wrong! In fact, it outputs "Foo(A)"

If I remove the virtual method from the C class, then it works as expected: "Foo(B)" is the output.

Why does the compiler choose the version that takes a A when B is the more-derived class?

Truthvalue answered 9/9, 2010 at 6:48 Comment(7)
Although I wont write such code, I find it a little surprising too :) Good question.Jolley
Hi. I added a constructor to both and put this in Foo(A a) a.GetType().Name and it says that it is type B.Baty
Could you please elaborate on the "abstract method from the C class" a bit? Did you remove it already?Heinrik
Beats me. Interesting question. BTW, I make the assumption that that the word "abstract" was really meant to be "virtual"? If I remove that method from C and remove the corresponding override keyword from D, the output becomes "Foo(B)".Kizzie
@Brian, @Fredrik: sorry, I changed the code to simplify it a bit (virtual methods work just as well as abstract ones for this problem) and forgot to change the text underneath :)Truthvalue
@leppie: I wouldn't want to write code like this either, but it's an inherited project :-)Truthvalue
@leppie: you wouldn't write such code? Why not, what's wrong with it? Suppose Foo(A) performs some kind of conversion to construct a B out of an A and then it calls Foo(B). Ooops.... stack overflow! And if the user calls Foo(new B()) he's really calling Foo(A) so Foo(A) has to check "is this really a B?" ... what a mess, and no compiler warnings...Nestor
T
15

The answer is in the C# specification section 7.3 and section 7.5.5.1

I broke down the steps used for choosing the method to invoke.

  • First, the set of all accessible members named N (N=Foo) declared in T (T=class D) and the base types of T (class C) is constructed. Declarations that include an override modifier are excluded from the set (D.Foo(B) is exclude)

    S = { C.Foo(B) ; D.Foo(A) }
    
  • The set of candidate methods for the method invocation is constructed. Starting with the set of methods associated with M, which were found by the previous member lookup, the set is reduced to those methods that are applicable with respect to the argument list AL (AL=B). The set reduction consists of applying the following rules to each method T.N in the set, where T (T=class D) is the type in which the method N (N=Foo) is declared:

    • If N is not applicable with respect to AL (Section 7.4.2.1), then N is removed from the set.

      • C.Foo(B) is applicable with respect to AL
      • D.Foo(A) is applicable with respect to AL

        S = { C.Foo(B) ; D.Foo(A) }
        
    • If N is applicable with respect to AL (Section 7.4.2.1), then all methods declared in a base type of T are removed from the set. C.Foo(B) is removed from the set

          S = { D.Foo(A) }
      

At the end the winner is D.Foo(A).


If the abstract method is removed from C

If the abstract method is removed from C, the initial set is S = { D.Foo(B) ; D.Foo(A) } and the overload resolution rule must be used to select the best function member in that set.

In this case the winner is D.Foo(B).

Termination answered 9/9, 2010 at 7:11 Comment(7)
However, that does not remove the method found in the base type, which is an exact match given the argument list. It think it's rather the following text "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." (here: msdn.microsoft.com/en-us/library/aa691356(VS.71).aspx) that describes the reason for this behavior. Since the non-virtual method in D is a match, methods from the base types are removed.Kizzie
@Fredrik, as per specs, overload resolution will not even happen here. Relevant sections are 7.5.5.1 (msdn.microsoft.com/en-us/library/aa691356(v=VS.71).aspx) that talks about method invocations. So candidate method set is created using section 7.3 and then overload resolution (7.4.2) may be applied to reduce set. In this case, lookup as per 7.3 will throw only one (non-virtual) method - no need to do overload resolution.Offering
yeah thats what i wanted to say but in am much more eloquent way.Baty
@VinayC: yes, that was sort of my point. Since there is only one matching method when the candidates have been identified by the compiler (given how I interpret the document that we both linked to), there is no overload resolution needed. According to the specs, the better-matching, but virtual method in the base class has been removed from the list in favor of the matching non-virtual method in the type at hand.Kizzie
@Fredrik, I had re-read and got your point. What you are saying is that rules from 7.3 will actually throw up two methods but reduction rules in 7.5.5.1 will remove the base method. I was under impression that 7.3 will not throw up two methods but I was actually wrong.Offering
Ah yes, that makes sense now... it's the override that causes the Foo() override in D to be removed.Truthvalue
Yuck! I'm in this situation right now and thinking about putting a public abstract void Foo(A a) in the base class, and sealed override void Foo(A a) in the derived class, just to fix the overload resolution. But what if I weren't allowed to change the base class, then what would I do? I could add a dynamic check in Foo(A a) to test if A is actually a B, but that's inefficient (and note that if it IS a B, I cannot pass it to Foo(B)! Stack Overflow!). I could rename Foo(A a) to FooWithA(A a) but it's kind of a kludge...Nestor
E
10

Why does the compiler choose the version that takes a A when B is the more-derived class?

As others have noted, the compiler does so because that's what the language specification says to do.

This might be an unsatisfying answer. A natural follow-up would be "what design principles underly the decision to specify the language that way?"

That is a frequently asked question, both on StackOverflow and in my mailbox. The brief answer is "this design mitigates the Brittle Base Class family of bugs."

For a description of the feature and why it is designed the way it is, see my article on the subject:

https://learn.microsoft.com/en-us/archive/blogs/ericlippert/future-breaking-changes-part-three

For more articles on the subject of how various languages deal with the Brittle Base Class problem see my archive of articles on the subject:

http://blogs.msdn.com/b/ericlippert/archive/tags/brittle+base+classes/

Here's my answer to the same question from last week, which looks remarkably like this one.

Why are signatures declared in the base class ignored?

And here are three more relevant or duplicated questions:

C# overloading resolution?

Method overloads resolution and Jon Skeet's Brain Teasers

Why does this work? Method overloading + method overriding + polymorphism

Enterotomy answered 9/9, 2010 at 14:57 Comment(10)
This is a horrible rule. If you change override to new in the derived class, the compiler behaves as everyone expects it to. I don't see how changing the behavior for public override vs public new has anything to do with the brittle base class problem. Since it's an override, the derived class author is clearly aware of the presence of the base class method. Is there anyone who is not baffled and annoyed by this? (If you are not baffled, huh? why not?)Nestor
@qwertie if you do not understand why this mitigates the BBC problem then read about it and think about it until you do would be my advice.Enterotomy
@Qwertie: Moreover: any knowledge possessed by the author of the derived class is irrelevant to this question. It is the customer of the author of the derived class who is the relevant party; why should that person have to know whether a method is overridden in a particular class as opposed to its base class? That's an implementation detail subject to change, not a part of the contract of the class!Enterotomy
WTF? The caller ("customer") has the same knowledge as the author. He knows he is calling class D. The caller goes to a D declaration, presses F12, and sees Foo(A a) and Foo(B a). The caller has no more reason to expect Foo(new B()) will invoke Foo(A a) than the class's author did! Nor does the caller have more reason to expect override to behave different than new than the original author did.Nestor
I kinda see what you're saying, but there are multiple ways this rule can shoot you in the foot. Have you considered this scenario: the author of D knows, or notices, that derived class methods "take precedence" over base class methods, so he realizes that defining Foo(A a) blocks access to the base class method Foo(B b). So he writes "override Foo(B b) {base.Foo(b);}" to avoid that unwanted behavior. Oops--it didn't work. Now what? I'd be surprised if you can find any scenario where this behavior is good. Nor is it intuitive (or this question would not have been asked).Nestor
@Qwertie: The customer does not have the same knowledge as the author; the author knows what class contains the override and why. Moreover, the author has the choice of overriding in the more derived class or implementing a third class that sits between the base and derived classes which does the override. Now: should overload resolution choose a different method when an override -- which is an implementation detail -- is moved towards the larger class in the hierarchy? That would be very strange indeed!Enterotomy
@Qwertie: To answer your question: there are many scenarios where this behavior is desirable. They are (1) when the author of the derived class knows more than the author of the base class. This is almost always the case! The author of the derived class knows better than the author of the base class what the semantics of each override should be for the derived class.Enterotomy
@Qwertie: And (2) the brittle base class scenarios, whereby the author of the base class adds a method after the author of the derived class has written the derived class. It is very strange that that author, who knows less than the author of the derived class, should be put in the position of deciding the behavior of the derived class when used by the customer consuming the derived class. That author has the least knowledge but you are putting them in charge of the decision! C#'s design mitigates this class of failures.Enterotomy
@Qwertie: Take a step back here. The behavior that you decry is the consequence of two rules. (1) Methods in derived classes were written by someone who knew more about the desired behavior than the author of the base class, therefore they take precedence over methods of the base class. That is, when faced with a choice, the compiler should default to preferring the method of the more derived class. (2) Choosing where in a hierarchy to override a virtual method is an invisible implementation detail, subject to change, and not part of the public surface of a class.Enterotomy
@Qwertie: Now, it is perfectly reasonable to say "those rules are reasonable, but the consequences of those rules working together produce a surprising conclusion, therefore we should abandon both rules". All designs are the result of a series of compromises between conflicting principles. The C# design team believes that both of those rules are sensible and moreover, that they mitigate an important class of failures seen in practice in other languages, and therefore decided that the benefits exceeded the costs. That does not make it a "horrible" rule; it makes it a reasonable rule.Enterotomy
I
2

I think it is because in case of a non-virtual method the compile time type of the variable on which the method is invoked is used.

You have the Foo method which is non-virtual and hence that method is called.

This link has very good explanation http://msdn.microsoft.com/en-us/library/aa645767%28VS.71%29.aspx

Infantile answered 9/9, 2010 at 7:2 Comment(0)
U
2

So, here is how it should work according to the specification (at compile time, and given that I navigated the documents correctly):

The compiler identifies a list of matching methods from the type D and its base types, based on the method name and the argument list. This means that any method named Foo, taking one parameter of a type to which there is an implicit conversion from B are valid candidates. That would produce the following list:

C.Foo(B) (public virtual)
D.Foo(B) (public override)
D.Foo(A) (public)

From this list, any declarations that include an override modifier are excluded. That means that the list now contains the following methods:

C.Foo(B) (public virtual)
D.Foo(A) (public)

At this point we have the list of matching candidates, and the compiler is now to decide what to call. In the document 7.5.5.1 Method invocations, we find the following text:

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.

This essentially means that if there is an applicable method declared in D, any methods from base classes will be removed from the list. At this point we have a winner:

D.Foo(A) (public)
Unscratched answered 9/9, 2010 at 7:36 Comment(1)
Yes, this clears things up quite a bit... now I just got to promise myself that I'll never write code like this :-)Truthvalue
B
0

I think that when implementing another class it looks as far up the tree to get an solid implementation of a method. As there is no method being called it is using the base class.

public void Foo(A a){
    Console.WriteLine("Foo(A)" + a.GetType().Name);
    Console.WriteLine("Foo(A)" +a.GetType().BaseType );
}

thats a guess i am no pro at .Net

Baty answered 9/9, 2010 at 7:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.