Question

My understanding (in C#) of how non-virtual methods are resolved is that it is dependent upon the type of the variable (and not the type of instance).

Take a look at the code below.

class Program
{
    static void Main(string[] args)
    {
        Sedan vehicle = new Sedan();
        vehicle.Drive();
        vehicle.Accelerate();
    }
}

abstract class VehicleBase
{
    public void Drive()
    {
        ShiftIntoGear();
        Accelerate();
        Steer();
    }

    protected abstract void ShiftIntoGear();
    protected abstract void Steer();

    public void Accelerate()
    {
        Console.WriteLine("VehicleBase.Accelerate");
    }
}

class Sedan : VehicleBase
{
    protected override void ShiftIntoGear()
    {
        Console.WriteLine("Sedan.ShiftIntoGear");
    }

    protected override void Steer()
    {
        Console.WriteLine("Sedan.Steer");
    }

    public new void Accelerate()
    {
        Console.WriteLine("Sedan.Accelerate");
    }
}

The Console Windows shows the following:

Sedan.ShiftIntoGear
VehicleBase.Accelerate
Sedan.Steer
Sedan.Accelerate

This doesn't make sense to me and I believe would throw many folks for a loop. If you now declare variable vehicle to be of type VehicleBase you get

Sedan.ShiftIntoGear
VehicleBase.Accelerate
Sedan.Steer
VehicleBase.Accelerate

Which is what I'd expect in the previous case as well because the method Accelerate is non-virtual.

In the previous output, (with the variable vehicle typed as a Sedan, I'd expect the Sedan.Accelerate to be called instead of VehicleBase.Accelerate. As it stands now, depending on where you call it from (from within the class or from outside) the behavior is changing.

It seems to me that the overload resolution rule(s) for re-introduced methods is taking precedence but I have a hard time believing that this is correct/expected behavior.

Was it helpful?

Solution 2

Non-virtual methods are resolved according to the type of the expression at compile time, and the call destination is fixed at compile time. The compiler looks up the method name in the symbol info available at compile time in the source code context of the call, and emits a call instruction to call the non-virtual method that it was able to find. It doesn't matter what the actual type of the instance is, the call always goes to the non-virtual method figured out at compile time from static type info.

Here is why the call to Accelerate goes to Sedan.Accelerate in the context of Main, but goes to VehicleBase.Accelerate in the context of the VehicleBase.Drive:

In the body of your Main function, you have declared a variable of type Sedan, and you make the method call using that variable. The compiler looks for the method named "Accelerate" in the type of the variable used to make the call, type Sedan, and finds Sedan.Accelerate.

Inside the method VehicleBase.Drive, the compile-time type of "self" is VehicleBase. This is the only type the compiler can see in this source code context, so that call to Accelerate in 'VehicleBase.Drive' will always go to VehicleBase.Accelerate, even if the runtime object instance's type is actually Sedan.

In the body of a method declared in the Sedan type, the compiler would resolve the non-virtual method call by looking in the methods of the Sedan type first, then looking at the VehicleBase type if no match were found in Sedan.

If you want the call destination to change based on the actual object instance type, you must use virtual methods. Using virtual methods will also give much more consistent execution results, since the call will always go to the most specific implementation determined by the object instance at runtime.

Non-virtual method call destinations are selected according to the compile-time type, with no awareness of runtime. Virtual method call destinations are selected according to the instance type at runtime.

OTHER TIPS

All this makes perfect sense - when you declare the vehicle as Sedan, the two calls to Accelerate are resolved differently:

  • When the call to Accelerate is made from the Drive method, it has no idea that there is a new method in the Sedan, so it makes the call to the corresponding method of the base
  • When the call to Accelerate is made from the Main method, the compiler knows that you are calling the new method, because it knows that the exact type of the vehicle variable is Sedan.

On the other hand, when the call to Accelerate is made from the Main method, but the variable is declared to be VehicleBase, the compiler cannot assume that the type is Sedan, so it resolves the Accelerate to the method of the base class again.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top