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:
- What are the contents of each slot?
- Which slot is called?
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.