Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 842
kasey Avatar asked Dec 02 '09 14:12

kasey


3 Answers

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.

like image 101
Lasse V. Karlsen Avatar answered Oct 30 '22 22:10

Lasse V. Karlsen


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)

like image 23
tvanfosson Avatar answered Oct 30 '22 23:10

tvanfosson


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. There are two reasons to choose the more derived method always over the less derived method.

The first is that the author of the derived class has much more information than the author of the base class. The author of the derived class knows everything about both the base class and the derived class, which is, after all, the class that the caller is actually using. When given the choice between calling a method written by someone who knows everything vs someone who knows only something about the type the caller is using, clearly it makes sense to prioritize calling the method written by the designer of the derived class.

Second, making that choice leads to a form of 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.

For a detailed explanation of how this rule protects you from the Brittle Base Class Failure, see my article on the subject.

And for articles on other ways that languages deal with Brittle Base Class situations, click here.

like image 40
Eric Lippert Avatar answered Oct 30 '22 23:10

Eric Lippert