views:

398

answers:

4

In the following code:

public abstract class MyClass
{
public abstract bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage);
}

public sealed class MySubClass : MyClass
{
    public override bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage)
    {
        return MyMethod(database, asset, ref errorMessage);
    }

    public bool MyMethod(
        Database database,
        AssetBase asset,
        ref string errorMessage)
    {
 // work is done here
}
}

where AssetDetails is a subclass of AssetBase.

Why does the first MyMethod call the second at runtime when passed an AssetDetails, rather than getting stuck in an infinite loop of recursion?

+8  A: 

C# will resolve your call to your other implementation because calls to a method on an object, where the class for that object has its own implementation will be favored over an overridden or inherited one.

This can lead to subtle and hard-to-find problems, like you've shown here.

For instance, try this code (first read it, then compile and execute it), see if it does what you expect it to do.

using System;

namespace ConsoleApplication9
{
    public class Base
    {
        public virtual void Test(String s)
        {
            Console.Out.WriteLine("Base.Test(String=" + s + ")");
        }
    }

    public class Descendant : Base
    {
        public override void Test(String s)
        {
            Console.Out.WriteLine("Descendant.Test(String=" + s + ")");
        }

        public void Test(Object s)
        {
            Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Descendant d = new Descendant();
            d.Test("Test");
            Console.In.ReadLine();
        }
    }
}

Note that if you declare the type of the variable to be of type Base instead of Descendant, the call will go to the other method, try changing this line:

Descendant d = new Descendant();

to this, and re-run:

Base d = new Descendant();

So, how would you actually manage to call Descendant.Test(String) then?

My first attempt looks like this:

public void Test(Object s)
{
    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Test((String)s);
}

This did me no good, and instead just called Test(Object) again and again for an eventual stack overflow.

But, the following works. Since, when we declare the d variable to be of the Base type, we end up calling the right virtual method, we can resort to that trickery as well:

public void Test(Object s)
{
    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Base b = this;
    b.Test((String)s);
}

This will print out:

Descendant.Test(Object=Test)
Descendant.Test(String=Test)

you can also do that from the outside:

Descendant d = new Descendant();
d.Test("Test");
Base b = d;
b.Test("Test");
Console.In.ReadLine();

will print out the same.

But first you need to be aware of the problem, which is another thing completely.

Lasse V. Karlsen
That is fairly insane, but thanks for clearing it up!
JustLoren
Since there is no way to get into this mess without having one virtual and one non-virtual method (you can't have two identical methods in the same class), or having a method in the base class call a method in a descendant class without having a virtual method, I don't think this factors into it. I'm still not sure about why that matters as you say, there are many such subtle problems, not all of them related to virtual methods. But please enlighten me, and I'll change my answer accordingly, I've only been shown this problem by Jon Skeet, any moment now he'll wake up and prove us all wrong...
Lasse V. Karlsen
As I understood it, he has, due to overriding the method from the base class, and also implementing it separately, two "identical" methods in his descendant class, and is wondering why the compiler picked one and not the other for his call.
Lasse V. Karlsen
@Lasse, Yes, I see that now, you are exactly right. - sorry I deleted my comment just before you responded.
Charles Bretana
Not to worry, this part of the compiler is the dark alley I don't dare venture into anyway, for fear of being mugged :)
Lasse V. Karlsen
Thanks very much, I was able to predict what your app would output thanks to your explanation, and it was the opposite of what I would have expected before, so I guess that means you answered my question! It's a relief to know there's finally a good reason for it.
kasey
@Lasse, the intriguing thing is looking at who ((what types) have access to the two implementations. objects of type MySubClass have access only to the non-virtual method. Objects that derive from MySubClass (if it wasn't sealed), would only have access to the virtual method if they were declared as their concrete type, and would only have access to the non-virtual method if they were declared as MySubClass. Given that it is sealed, and his base is abstyract, in his example there should be no way to access the virtual member at all, no?
Charles Bretana
Well, you can internally, through some trickery, let met change my answer.
Lasse V. Karlsen
And with my changes, you will also in this case have to resort to a cast, so it's a bit contrived.
Lasse V. Karlsen
+1  A: 

Because that's the way the language is defined. For virtual members, the Implementation which is called at runtime, when a method exists in both a base class and a derived class, is based on the concrete type of the object which the method is called against, not the declared type of the variable which holds the reference to the object. Your first MyMethod is in an abstract class. So it can never be called from an object of type MyClass - because no such object can ever exist. All you can instanitate is derived class MySubClass. The concrete type is MySubClass, so that implementation is called, no matter that the code that calls it is in the base class.

For non-virtual members/methods, just the opposite is true.

Charles Bretana
Sorry, to clarify: by the "first" MyMethod I meant the first implementation, i.e. the first MyMethod in MySubClass, but the second lexical instantiation.What's confusing me is why the final occurrence of MyMethod is called when passed an AssetDetails.
kasey
@kasey, Yes, you're correct in that I mis-understood yr question... lasse' answer above is right on target...
Charles Bretana
+5  A: 

See the section of the C# Language Specification on Member Lookup and Overload Resolution. The override method of the derived class is not a candidate because of the rules on Member Lookup and the base class method is not the best match based on the Overload Resolution rules.

Section 7.3

First, the set of all accessible (Section 3.5) members named N declared in T and the base types (Section 7.3.1) of T is constructed. Declarations that include an override modifier are excluded from the set. If no members named N exist and are accessible, then the lookup produces no match, and the following steps are not evaluated.

Section 7.4.2:

Each of these contexts defines the set of candidate function members and the list of arguments in its own unique way, as described in detail in the sections listed above. For example, the set of candidates for a method invocation does not include methods marked override (Section 7.3), and methods in a base class are not candidates if any method in a derived class is applicable (Section 7.5.5.1). (emphasis mine)

tvanfosson
This is the best technical answer to this question.
Lasse V. Karlsen
+1  A: 

As others have correctly noted, when given the choice between two applicable candidate methods in a class, the compiler always chooses the one that was originally declared "closer" to the class which contains the call site when examining the base class hierarchy.

This seems counterintuitive. Surely if there is an exact match declared on a base class then this is a better match than an inexact match declared on a derived class, yes?

No. Making that choice leads to the Brittle Base Class Failure. We wish to protect you from this failure, and therefore have written the overload resolution rules so as to avoid it whenever possible.

My article on this subject is here:

http://blogs.msdn.com/ericlippert/archive/2007/09/04/future-breaking-changes-part-three.aspx

Eric Lippert
Thanks; it's very interesting to see the rationale behind the decision.
kasey