public class A
{
public virtual void Write() { Console.Write("A"); }
}
public class B : A
{
public override void Write() { Console.Write("B"); }
}
public class C : B
{
new public virtual void Write() { Console.Write("C"); }
}
public class D : C
{
public override void Write() { Console.Write("D"); }
}
// ...
D d = new D();
C c = d;
B b = c;
A a = b;
d.Write();
c.Write();
b.Write();
a.Write();
Why is the output DDBB and not DDDD?
Why doesn't D Write() override B Write() if the actual object that b is referencing is D?
To truly understand what's going on we have to have the mental model of how the type's list of methods (its method table) is maintained to allow for virtual calls at runtime. It is contiguous with 3 regions in this order.
If we take your example class by class:
public class A {
public virtual void Write() { Console.Write("A"); }
}
virtual
by itself creates new virtual slot for the method in region 2. It already inherited 4 methods from System.Object
, so the method table of A
looks like this:
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
--- end of region 1
--- start of region 2
5. Write (A::Write)
--- end of region 2
--- start of region 3
--- end of region 3
Next we have override
for B
public class B : A {
public override void Write() { Console.Write("B"); }
}
This reuses the slot we just declared in A
, so B
's table looks like this (Write
is now in region 1, inherited virtual methods)
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (still A::Write)
--- end of region 1
--- start of region 2
--- end of region 2
--- start of region 3
--- end of region 3
Then in C we have new virtual
public class C : B {
public new virtual void Write() { Console.Write("C"); }
}
This lets us declare a new virtual slot for this implementation, again in region 2:
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (A::Write)
--- end of region 1
--- start of region 2
6. Write (C::Write)
--- end of region 2
--- start of region 3
--- end of region 3
Finally, if we modified your example for D
a little to not override but just use new
public class D : C {
public new void Write() { Console.Write("D"); }
}
This declares a new slot in region 3
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (A::Write)
6. Write (C::Write)
--- end of region 1
--- start of region 2
--- end of region 2
--- start of region 3
7. Write (D::Write)
--- end of region 3
The c# compiler does the disambiguating which Write
method to call by actually emitting different IL based on the reference type it got:
D::Write
C::Write
A::Write
It goes from bottom to top of the method table for the type of the reference of the variable making the method call and looks for a matching name and then emits different IL method calls for the different slots:
D
reference -> slot #7 (D::Write)C
reference -> slot #6 (C::Write)B
reference -> slot #5 (A::Write)A
reference -> slot #5 (A::Write)Thus for the modified example the IL be this:
// D d = new D();
IL_0001: newobj instance void D::.ctor()
// C c = d;
// B b = c;
// A a = b;
// d.Write();
IL_000e: callvirt instance void D::Write()
// c.Write();
IL_0015: callvirt instance void C::Write()
// b.Write();
IL_001c: callvirt instance void A::Write()
// a.Write();
IL_0023: callvirt instance void A::Write()
and the output DCBB
.