I was recently given the following piece of code as a sort-of puzzle to help understand Polymorphism
and Inheritance
in OOP - C#.
// No compiling!
public class A
{
public virtual string GetName()
{
return "A";
}
}
public class B:A
{
public override string GetName()
{
return "B";
}
}
public class C:B
{
public new string GetName()
{
return "C";
}
}
void Main()
{
A instance = new C();
Console.WriteLine(instance.GetName());
}
// No compiling!
Now, after a long, long chat with the other developer who presented the puzzle, I know what the output is, but I wont spoil it for you. The only issue I'm really having is how we get to that output, how the code steps through, what's inheriting what, etc.
I thought C
would be returned as that seems to be the class that is defined. Then I went through my head as to whether B
would be returned because C inherits B
- but B
also inherits A
(which is where I got confused!).
Could anyone explain how polymorphism and inheritance play their part in retrieving the output, eventually displayed on screen?
The correct way to think about this is to imagine that every class requires its objects to have a certain number of "slots"; those slots are filled with methods. The question "what method actually gets called?" requires you to figure out two things:
Let's start by considering the slots. There are two slots. All instances of A are required to have a slot we'll call GetNameSlotA. All instances of C are required to have a slot we'll call GetNameSlotC. That's what the "new" means on the declaration in C -- it means "I want a new slot". Compared to the "override" on the declaration in B, which means "I do not want a new slot, I want to re-use GetNameSlotA".
Of course, C inherits from A, so C must also have a slot GetNameSlotA. Therefore, instances of C have two slots -- GetNameSlotA, and GetNameSlotC. Instances of A or B which are not C have one slot, GetNameSlotA.
Now, what goes into those two slots when you create a new C? There are three methods, which we'll call GetNameA, GetNameB, and GetNameC.
The declaration of A says "put GetNameA in GetNameSlotA". A is a superclass of C, so A's rule applies to C.
The declaration of B says "put GetNameB in GetNameSlotA". B is a superclass of C, so B's rule applies to instances of C. Now we have a conflict between A and B. B is the more derived type, so it wins -- B's rule overrides A's rule. Hence the word "override" in the declaration.
The declaration of C says "put GetNameC in GetNameSlotC".
Therefore, your new C will have two slots. GetNameSlotA will contain GetNameB and GetNameSlotC will contain GetNameC.
We've now determined what methods are in what slots, so we've answered our first question.
Now we have to answer the second question. What slot is called?
Think about it like you're the compiler. You have a variable. All you know about it is that it is of type A. You're asked to resolve a method call on that variable. You look at the slots available on an A, and the only slot you can find that matches is GetNameSlotA. You don't know about GetNameSlotC, because you only have a variable of type A; why would you look for slots that only apply to C?
Therefore this is a call to whatever is in GetNameSlotA. We've already determined that at runtime, GetNameB will be in that slot. Therefore, this is a call to GetNameB.
The key takeaway here is that in C# overload resolution chooses a slot and generates a call to whatever happens to be in that slot.