9

Previously I have asked a question that was answered not fully, hence I have decided to re-formulate my question to understand what is going on:

Here is my class hierarchy:

interface I
{
    void f();
}

class A : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> A ");
    }
}

class B : A
{
    // non overriding but hiding class A method
    public void f()
    {
        Debug.Log("---->> B ");
    }
}

class C : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> C ");
    }
}

Here is the execution code:

Random rnd = new Random();
var randomI = rnd.Next(0, 2);

I i = null;
if (randomI == 0)
{
     i = new B(); 
}
else
{
    i = new C();
}
i.f(); 

As it is right now, it will either output A or C. It won't output B.

Here is the question: Could you please explain how the decision is made what function to call by covering these steps?

  1. When the decision is made what function to call - runtime or compile time?
  2. What is the mechanism how to decide what function to call?
mjwills
  • 23,389
  • 6
  • 40
  • 63
Narek
  • 38,779
  • 79
  • 233
  • 389
  • 5
    @Neil why you assume this code is used somewhere, it can be for educational purposes? – Darjan Bogdan Sep 06 '18 at 13:02
  • I've come across (professional) code that uses these edge cases and when a new developer sees it, they don't understand why it sometimes works, it invariably involves lots of googling/SO and misunderstanding. I presume you are getting compiler warnings. Listen to them. – Neil Sep 06 '18 at 13:05
  • Is this inside unity? – SᴇM Sep 06 '18 at 13:08
  • @Narek If you want `B` shown, change `B` to `class B : A, I`. – mjwills Sep 06 '18 at 13:11
  • There's a reason that hiding the method in `B` produces a compiler warning -- it's exactly to alert you to the fact that methods will not be invoked in an obvious way. – Jeroen Mostert Sep 06 '18 at 13:12
  • I'd strongly suggest to look into IL code. For example last call to `i.f();` is compiled to `IL_0034: callvirt instance void ConsoleApp1.I::f()`... Since the type of `i` is unknown during compilation this `I::f` will be inferred at run time. The only difference from your previous question is that now the type is known at runtime but compiler *knows* which function to call for each type at compile time – Fabjan Sep 06 '18 at 13:23
  • B does not itself implement I which means that if you cast an object of either A or B type to I, it will map to A's methods. If you had made methods in A virtual, and overridden them in B, or implemented I again in B, casting an object of type B to I would use B's methods. – Lasse V. Karlsen Sep 06 '18 at 13:23
  • Also, please make sure to encourage answers to answer your question fully, instead of leaving a question with, in your opinion, half an answer and just opening up a new question, which I somewhat view as a duplicate of your previous question. – Lasse V. Karlsen Sep 06 '18 at 13:24
  • Possible duplicate of [Difference between new and override](https://stackoverflow.com/questions/1399127/difference-between-new-and-override) and [C# - Keyword usage virtual+override vs. new](https://stackoverflow.com/questions/159978/c-sharp-keyword-usage-virtualoverride-vs-new) – Dmitry Pavliv Sep 06 '18 at 13:41
  • @DmitryPavliv I would suggest they aren't the best duplicates since they don't speak **specifically** about interface implementation inheritance. They are good links for showing another way to solve it - yes. But not **why** it works this way. – mjwills Sep 06 '18 at 13:44

4 Answers4

4

During compile-time it binds a call to I interface, and then during run-time it calls a top method in inheritance chain which implements I.f().

So, in your code this

A a = new A();
a.f();

B b = new B();
b.f();

will make compiler do the following instructions:

  • create an instance of class A and assign to "a"
  • take an object assigned to "a" and call the method, which is on the top of inheritance chain and which implements A.f(). it is A.f() itself in this case
  • create an instance of class B and assign
  • take an object assigned to "a" and call the method, which is on the top of inheritance chain and which implements B.f(). it is B.f() itself in this case

which results into "A" and "B".

However, when you make this:

I i;
i = new B();
i.f();

you make it compile the following instructions:

  • declare a variable "i"
  • create a new object B and assign it to "i"
  • take an object assigned to "i" and call the method, which is on the top of inheritance chain and which implements I.f(). It is A.f(), because class B does not implement interface I

At i.f() line it doesn't know that new B() was assigned to i, it could be passed from somewhere else. It just knows that there is some abstract object instance which implements I and it needs to call its f() method.

You can think of new methods like of methods with different name:

public class B : A
{
    // non overriding but hiding class A method
    public void anotherName()
    {
       Debug.Log("---->> B ");
    }
} 

A a = new A();  
a.f();
B b = new B();
b.anotherName();

I i = new B();
i.f(); // this will obviously call the `A.f()` because there is no such method in B

The only difference is that you can't call hidden method for inherited class instance.

Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101
  • How is that compile time? – Fildor Sep 06 '18 at 13:16
  • 1
    @Fildor It was sort of misguiding, I have updated the answer – Yeldar Kurmangaliyev Sep 06 '18 at 13:17
  • **which is on the top of inheritance chain** sounds kind of confusing. To me you mean the one found in the lowest subclass - the one that inherits the last and implements `I`, is that correct? – Narek Sep 07 '18 at 05:35
  • To this answer I would add @ikkentim's answer from here https://stackoverflow.com/questions/52203479/c-sharp-call-an-interface-method-non-virtual-implementation particularly the last part: *Interface implementations are marked as virtual in IL, even though it wasn't marked virtual in C#. The method is also marked as final in IL, if the method would've been virtual in C#, it would not have been marked as final.* . This helps A LOT to understand the dispatching mechanism. – Narek Sep 07 '18 at 05:41
4

When the decision is made what function to call - runtime or compile time?

At compile time the compiler determines that A.f is the method to call if someone casts B to I and calls f on it. At runtime it calls that method if an instance of B (vs say an instance of C) is involved. In other words, the key decision is being made at compile time.

Note that if the method was virtual then see @YeldarKurmangaliyev's answer for how it calls the "top method in inheritance chain" (but that isn't the scenario here).

What is the mechanism how to decide what function to call?

The relevant part of the specification is 13.4.5 Interface implementation inheritance:

A class inherits all interface implementations provided by its base classes. Without explicitly re-implementing an interface, a derived class cannot in any way alter the interface mappings it inherits from its base classes.

This is why class B : A shows A but class B : A, I shows B. Since with the latter you are explicitly re-implementing the interface.

Example from the specification (which is basically your scenario):

A class inherits all interface implementations provided by its base classes. Without explicitly re-implementing an interface, a derived class cannot in any way alter the interface mappings it inherits from its base classes. For example, in the declarations

interface IControl
{
    void Paint();
}
class Control: IControl
{
    public void Paint() {...}
}
class TextBox: Control
{
    new public void Paint() {...}
}

the Paint method in TextBox hides the Paint method in Control, but it does not alter the mapping of Control.Paint onto IControl.Paint, and calls to Paint through class instances and interface instances will have the following effects

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();          // invokes Control.Paint();
t.Paint();          // invokes TextBox.Paint();
ic.Paint();         // invokes Control.Paint();
it.Paint();         // invokes Control.Paint();

The specification also talks about using virtual (which is a more common solution than explicitly specifying that B implements I):

However, when an interface method is mapped onto a virtual method in a class, it is possible for derived classes to override the virtual method and alter the implementation of the interface.

mjwills
  • 23,389
  • 6
  • 40
  • 63
1

IMHO, the intersting part is why: ((I)new B()).f() prints

---->> A

Casting B as I, the base class method will be used. If you want to print ---->> B instead, and keep the if/else branching, you'll have to cast i as B which will call explicitly B's implementation:

if (i is B)
   ((B)i).f();
else
   i.f();

When casting to I, that's what's going on with your classes declarations:

I -> A -> B
|_ f() is implemented in subclasses, let's go one step down;

I -> A -> B
     |_ f() is found, let's call A's f();

If you want cast to call B's implementation, make B implement I directly:

class B : A, I

Thus, when casting to I, this will happen:

// Paths from I to B
I -> A -> B 
I -> B // Shorter path, let's go via this one.

I -> B
|_ f() is implemented in subclasses, let's go one step down;

I -> B
     |_ f() is found, let's call B's f();

Of course, this is a simpler version of what's really going on, but it helps understanding the concept

Thomas Ayoub
  • 29,063
  • 15
  • 95
  • 142
1

I think this is particularly likely to confuse people who (like me) learned about virtual vs. non-virtual methods in C++, and about interfaces in Java, and later had to figure out how the two might interact in C#.

Method dispatch through an interface reference in Java is (comparatively) straightforward, because all methods in Java are virtual. So of course the instance runtime type decides what's invoked and that's that... in Java.

This does mean that certain more complex relationships between instance, method name, and implementation are not available in Java, which can be argued as "a good thing" or "a bad thing", depending on your point of view.

Regardless, this is one of the big differences between Java and C#, because not only are methods not required to be virtual in C#, they aren't by default. And without getting into implementation details, that leads to a key insight:

If a method isn't marked virtual in a parent class, then you can't mark a corresponding (same name and signature) method in the child class as override. And, separately, if a method in a child class isn't marked as override, then it behaves as new whether or not it had been virtual in the base class.

So... if a class implements an interface with a non-virtual method, is virtual-like dispatch used when you want to invoke the method through an interface reference? It would seem like a reasonable way to do it. (Think of all the ways an interface reference could end up pointed at an instance of a particular class, that might not be knowable when the code using the interface reference is compiled. Virtual dispatch just makes sense here.) Still it's not the only way.

BUT:

It doesn't matter. Even if it is, in C# virtual doesn't mean that a child-class implementation has to override the base-class method; it merely means that it can if declared to do so. And by not explicitly declaring the base-class method virtual, you make it impossible to compile a child class with a method that declares itself an override of that method.


Update : The currently-top-rated answer claims that the key decision is made at compile time, and while an implementation could do that in the specific example, I don't believe it does - simply because that approach doesn't generalize.

public void myMethod(I i) {
    i.f();
}

When compiling the above method, the compiler has absolutely no clue what actual implementation it should invoke. And when compiling some other unit of code with the line

myMethod(new A());

the compiler doesn't know that f() specifically will need to be resolved. So the compiler, handling this second block, can set up information to be used by the output from compiling the first block, in deciding how to dispatch any given method.

But ultimately the decision to actually execute a given method implementation can't be made until runtime; whether that takes the form of virtual dispatch, or some reflection-based monstrosity, or whatever.

At the language level, that's all just implementation details. The specified behavior is what's important, and that is where the relationship between the virtual and override keywords becomes key.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52