Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

More about Virtual / new...plus interfaces!

Yesterday I posted a question about the new/virtual/override keywords, and i learned a lot from your answers. But still i remain with some doubts.

In between all the "boxes", i lost touch with what's really happening to the type's method tables. For instance:

interface I1 { void Draw(); }
interface I2 { void Draw(); }

class A : I1, I2
{
    public void Minstance() { Console.WriteLine("A::MInstance"); }
    public virtual void Draw() { Console.WriteLine("A::Draw"); }
    void I2.Draw() { Console.WriteLine("A::I2.Draw"); }
}
class B : A, I1, I2
{
    public new virtual void Draw() { Console.WriteLine("B::Draw"); }
    void I1.Draw() { Console.WriteLine("B::I1.Draw"); }
}

class Test
{

    public static void Main()
    {
        A a = new B();
        a.Draw();
        I1 i1 = new A();
        i1.Draw();
        I2 i2 = new B();
        i2.Draw();
        B b = (B)a;
        b.Draw();
    }

}
}

The question thats asked on this exercise is: Fill in the types' method tables according to the code, and explain the output generated by running the Main().

My answer was: In type A we have 3 methods: MInstance(), Draw()- the A::Draw version - and I2::Draw In type B we have 4 methods: MInstance from A, B::Draw, I1::Draw and I2::Draw

I'm not very confident about my answer, and thats why i'm posting this question. When we implement interfaces, it's created a new slot on the method table for the methods of said interface? shouldnt we be implementing I2::Draw too in class A?

Likewise, when we call a method using an interface variable (like i1.Draw()) i understand we're on dynamic dispatch, and therefore we should look at the type of the object being held by the variable(type A in that case) and search A's method table for a method called specifically I1.Draw. But what if we don't find it? How should i proceed in these cases? Is there any rule of thumb i should know about in order to successfully tackle these issues?

Sorry for being so boring with this question, but i really need to untie this knot on my head ;)

Cheers!

like image 524
Bruno Avatar asked Dec 22 '22 06:12

Bruno


1 Answers

Good question.

The way to think about this is: interfaces get their own set of slots. A class which implements an interface is required to fill in those slots.

  • Interface I1 has a slot we'll call I1SLOT.
  • Interface I1 has a slot we'll call I2SLOT.
  • Class A has two slots of its own, AMinSLOT and ADrawSLOT.
  • Class A has three methods, which we'll call AMinMethod, ADrawMethod and AI2DrawMethod.
  • When you say "new A", the runtime has four slots to fill in.
  • I1SLOT is filled in with ADrawMethod.
  • I2SLOT is filled in with AI2DrawMethod.
  • AMinSLOT is filled in with AMinMethod.
  • ADrawSLOT is filled in with ADrawMethod.
  • Class B has three slots. It inherits AMinSLOT and ADrawSLOT, and defines a new slot, BDrawSLOT.
  • Class B has two methods, BDrawMethod and BI1DrawMethod.
  • When you say "new B" the runtime has five slots to fill in.
  • I1SLOT is filled in with BI1DrawMethod.
  • I2SLOT is filled in with BDrawMethod.
  • AMinSLOT is filled in with AMinMethod.
  • ADrawSLOT is filled in with ADrawMethod.
  • BDrawSLOT is filled in with BDrawMethod.

Now remember, the job of overload resolution is to choose the slot based on the type and the arguments. There are no arguments, so the compiler only has the type to go off of.

  • When you call Draw on an object of compile-time type A, the best match is ADrawSLOT.
  • When you call Draw on an object of compile-time type B, the best match is BDrawSLOT.
  • When you call Draw on an object of compile-time type I1, the best match is I1SLOT.
  • When you call Draw on an object of compile-time type I2, the best match is I2SLOT.

And the compiler generates code that says "call whatever method is in the chosen slot at runtime."

Summing up:

A a1 = new A();
A a2 = new B();
B b = new B();
(a1 as A).Draw();  // ADrawSLOT contains A::Draw
(a1 as I1).Draw(); // I1SLOT    contains A::Draw
(a1 as I2).Draw(); // I2SLOT    contains A::I2.Draw
(a2 as A).Draw();  // ADrawSLOT contains A::Draw
(a2 as B).Draw();  // BDrawSLOT contains B::Draw
(a2 as I1).Draw(); // I1SLOT    contains B::I1.Draw
(a2 as I2).Draw(); // I2SLOT    contains B::Draw
(b as A).Draw();   // ADrawSLOT contains A::Draw
(b as B).Draw();   // BDrawSLOT contains B::Draw
(b as I1).Draw();  // I1SLOT    contains B::I1Draw
(b as I2).Draw();  // I2SLOT    contains B::Draw

If you're interested in how this is implemented, use ILDASM to disassemble your program, and then look at metadata table 25 (0x19), the MethodImpl table. This program's MethodImplTable is:

1 == 0:TypeDef[2000004], 1:MethodDefOrRef[06000005], 2:MethodDefOrRef[06000002]
2 == 0:TypeDef[2000005], 1:MethodDefOrRef[06000008], 2:MethodDefOrRef[06000001]

Then you can look in the typedef and methoddef tables, and you'll see that this decodes as:

in type A the method A::I2.Draw implements the method I2::Draw
in type B the method B::I1.Draw implements the method I1::Draw

The MethodImpl table is how the CLI represents the notion of "I need to stick something in this slot that is different than what the regular name matching rules would choose". Normally the name matching rules would choose a method called "Draw" to go in that slot, not "I1.Draw" or "I2.Draw".

You might also want to read section 22.27 of Partition II of the CLI spec.

like image 149
Eric Lippert Avatar answered Jan 02 '23 22:01

Eric Lippert