2

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.

Shiv Kumar
  • 9,599
  • 2
  • 36
  • 38
  • Not sure what you are finding perplexing here. Can you explain what you are finding to be a problem? – Oded Dec 15 '12 at 20:13
  • I thought I did. The two outputs have been provided and my expectation as well. I'll edit my post to explain my expectation more clearly – Shiv Kumar Dec 15 '12 at 20:17
  • If you make `Accelerate` virtual in `VehicleBase` and override it in `Sedan` you'll get the behavior you are expecting. – juharr Dec 15 '12 at 20:22
  • @juharr, the whole point of this question is that the method *is* non-virtual. So I don't want to make it virtual. – Shiv Kumar Dec 15 '12 at 20:24
  • @ShivKumar You either make in virtual or you get the behavior you are seeing, simple as that. 9 times out of 10 using new on a method is just bad design. – juharr Dec 16 '12 at 04:31
  • @juharr, I agree that one needs a very good reason to re-introduce a method in a descendant class. Similarly, having a public abstract/virtual method is bad design too. I don't agree that the behavior I'm seeing is "understandable". Yes, one can always just agree with the status quo leave things alone. I just think this behavior will throw folks for a loop. – Shiv Kumar Dec 16 '12 at 04:37

2 Answers2

4

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.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • since Accelerate is a non-virtual method, and the variable has been typed as Sedan, the compiler *does* know that the method has been re-introduced (new) and should call the re-introduced Accelerate method from both places. – Shiv Kumar Dec 15 '12 at 20:47
  • @ShivKumar The compiler knows it only when you declare the variable as `Sedan`, i.e. in the way the code is written in your post. However, when you declare the variable as `VehicleBase vehicle = new Sedan()`, the compiler knows that the type of `vehicle` is `VehicleBase`, so it cannot call the newly added method. – Sergey Kalinichenko Dec 15 '12 at 20:54
  • The case where the variable is typed to be a Sedan is where (in my opinion) the behavior is unexpected. So if it knows the type to be a Sedan at compile time, and the method in question is non-virtual, why does it behave the way it does? Maybe if you read my post again, it'll help :) – Shiv Kumar Dec 15 '12 at 21:11
  • @ShivKumar I think I did read your post correctly. You invoke `Accelerate` from two places. In one place, the compiler knows it's `Sedan`; in the other place, the compiler does not know it's `Sedan`. Specifically, in `Main` the compiler knows the exact type to be `Sedan`, but in the `VehicleBase.Drive` the compiler does not know it's `Sedan`. As a consequence, the `new` method is called only from `Main`, but not from the `Drive` method. – Sergey Kalinichenko Dec 15 '12 at 21:17
  • I *don't* understand how the compiler *doesn't* know the variable is of type Sedan. It is declared to be as such. IMHO, there is something else going on here. Nonetheless, I have a hard time believing this is "expected" behavior. One can always rationalize this but to think we have this behavior in the language as "as designed" is hard to believe because I can't imagine where/when I'd use this behavior. – Shiv Kumar Dec 15 '12 at 21:30
  • @ShivKumar "I don't understand how the compiler doesn't know the variable is of type Sedan. It is declared to be as such." Not to the `Drive` method of the `VehicleBase`, it isn't! To the `Drive` method, the variable is the implicit `this` variable of the instance method, not the `vehicle` variable of the `Main` method. In fact, `Drive` has absolutely no idea that the `Sedan` class even exists, let alone its `Accelerate` method or the `vehicle` variable. Think of the `Drive` method as being in a separately compiled unit. There is no `Accelerate` method other than that in the `VehicleBase`. – Sergey Kalinichenko Dec 15 '12 at 22:15
  • I'm afraid you're not making sense :) (to me). Drive is now in the type Sedan (inherited). "this" is a type Sedan too. However, since the methods (Drive and Accelerate) are non-virtual the compiler should be able to determine what method to call at compile time. It should give precedence to the new Accelerate method since it is declared in the type in question. – Shiv Kumar Dec 15 '12 at 22:52
  • @ShivKumar Here, I believe, lies the key to your misunderstanding: "`Drive` is now in the type `Sedan` (inherited)." No, it is not! `Drive` remains in the type `VehicleBase`. It happens to be accessible to the `Sedan` by virtue of being inherited, but that does not change a thing: `Drive` method thinks that it is executing in the context of a `VehicleBase`, thus paying attention to overrides of only `virtual` functions. If `Accelerate` were virtual, the `Drive` would pay attention to the override. But since it isn't, `Drive` calls the base version of `Accelerate` - the only one it knows about. – Sergey Kalinichenko Dec 15 '12 at 23:36
1

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.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
  • thanks for your explanation and clarifying the reasons for why VehicleBase.Accelerate is called from the Drive method. The key to me getting this was that the *compile-time* type of self/this is VehicleBase and not the type of the variable. So even though I now understand the explanation, I don't quite get why the compiler can't see the type to be Sedan (inside the Drive method) while compiling :). – Shiv Kumar Dec 18 '12 at 13:39
  • Would Delphi behave similarly? I frequently used static method dispatching to my advantage and never tripped over this Probably never re-introduced methods in descendant classes while calling them from the base class. What about Java (do you know?) – Shiv Kumar Dec 18 '12 at 13:42
  • The only difference between C# and Delphi behavior is that Delphi was doing it 6 years prior to C#. ;> – dthorpe Dec 18 '12 at 17:41
  • In Delphi terms, think of it like this: Delphi compiles top-down. The Sedan class appears after VehicleBase.Drive. So the compiler hasn't even parsed the Sedan class yet when it is compiling and emitting the machine code for VehicleBase.Drive. So, for Delphi at least, there is no possible way that it could know about the Sedan class in the context of the VehicleBase.Drive method. C# is a multipass parser, so this argument doesn't hold there. – dthorpe Dec 18 '12 at 17:55
  • So the argument doesn't hold....therefore this behavior is "by design". And I'm guessing it was in Delphi as well? – Shiv Kumar Dec 19 '12 at 17:58
  • Yes, it's by design. The "reintroduced" method name is only accessible when it is reachable by the compile-time type of the variable (including self) used to make the call. – dthorpe Dec 19 '12 at 20:50