The following both over-simplifies and treats one possible implementation as fact, but should suffice to have a working mental model.
When calling code "knows about" a class it knows the following things:
Fields can be accessed at a particular offset from the object's location. So for example if an object is at address 120, and has two integer fields, then it might be able to access one them at address 124. If another object of the same type was at address 140, the equivalent field would be at 144.
Non-virtual methods (and properties can be considered syntactic sugar on one or two methods) are functions at a particular address that take a reference to the object you are calling on (this
from within the method) and the other parameters of that function.
Virtual methods are like the above, but their address can be found by looking at a particular offset within a table associated with the class, the address of which will also be a particular offset from the address of the class.
In this, Kid
has a table of methods that is a superset of that of Parent
(it could add more methods), and which has the same function address for those methods it didn't over-ride (calling Equals
on it uses the same function as calling Equals
on a Parent
), but a different address for those it does over-ride (Print()
in this case).
Hence if you have a Kid
then whether you have it through a Parent
reference or a Kid
reference, calling Print()
will look to the same table, look up the location of the Print()
method, and call it.
In the case of Child
, there is new
used on the Print
method. This tells the compiler that we specifically want a different table. Hence, if we call Print()
via a Child
reference, it looks up the Child
-specific table, and calls the method it finds. If however we call it via a Kid
or Parent
reference, then we don't even know that there is a Child
-specific table we could be using and we look up the function in the table we know Kid
and Parent
have respectively, and call the function found (that defined in Kid
).
As a rule, new
is to be avoided. It's use is in two places:
One is backwards compatibility. If for example Child
had a Name
property, and then later on the code for Parent
was changed so that it too had a Name
property, we've a conflict. Since Child
's Name
isn't an override, it gets treated as if it had new
but gives us a warning, as this is the only way code using the old way of things and code that knows about the new Name
on Parent
can co-exist. If we ever come back to re-compile Child
we should probably either refactor so it doesn't have its own Name
(if that on Parent
does what we want), refactor so it is an override, refactor to something completely different, or add new
to indicate that this is how we want things to be despite it being less than ideal.
The other is when new
allows for a more specific form of the same behaviour that the base class' method allows, but is logically compatible (so users don't get surprised). This latter should go in the semi-advanced techniques box and not be done lightly. It should also be commented as such, because most of the time seeing new
means your dealing with something that is at best a compromise and should probably be improved.
(Aside: am I the only person who thought of tabloid newspapers upon seeing Kid
s having Child
ren?)