Explaining virtual methods in C# with words and pictures

In this post I will try to explain virtual methods in C#.  There are similar concepts in other languages, like C++ and Java, but I can’t vouch that all the details I go into here will apply exactly to any other language.  The whole post is quite long, but I want to get to a slightly weird destination in a series of small and hopefully easy to understand steps.

To start with I will go through a very simple example that lays the foundations for the later examples.  It will introduce you to a diagram that I hope will prove useful later, via a gentle reminder of inheritance.  There might be times when you want to compare two examples, e.g. to see how a code change leads to a behaviour change.  I suggest you bring this article up in two different browser windows and have them side-by-side on the screen so you can scroll them to different positions.

The major difference between the article you’re reading now and other articles that I’ve seen is that as well as listings and output, I will try to explain things using diagrams.  This is an area that only properly clicked for me once I’d got out paper and pen to draw a diagram that would explain the behaviour I was seeing.  I have no idea if this is what’s actually happening behind the scenes, but it fits the evidence and also works for me.

1. Simple inheritance

This is almost as simple as it could get but is mostly to introduce the source code that will act as a framework for later examples, and to introduce the diagrams.

Class A is the parent class for B, which in turn is the parent class for CA defines a public method X, which is therefore visible to B and C.  (It could also have been protected rather than public.)  All the examples will be like this – one method X, and a set of classes named after the letters of the alphabet that derive from each other in this kind of straight line.

In the calling code there’s an important pair of related things to keep separate in your mind.  One is the type of the variable, and the other is the type of the value held by the variable.  For the first three variables – a, b and c – these are the same.  The variable a has the type A and stores a value that is also of type A and so on.

For each of the next three variables, the type of the variable is different from the type of the value it holds.  In all cases, the value’s type is a child or grand-child type of the variable’s type.

Seeing as we have only one definition of any method, it’s probably not surprising that the output is the same from all variables (apart from the label).  Note that the main method is common to all the examples apart from the last one (which is bigger), and so it will be shown here but not repeated in the examples below.  Its job is just to call the classes in our class hierarchy in a variety of ways.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;

namespace Inheritance
{
    class Program
    {
        static void Main()
        {
            A a = new A();
            B b = new B();
            C c = new C();

            A bInA = new B();

            A cInA = new C();
            B cInB = new C();

            a.X(1, "a");
            b.X(2, "b");
            c.X(3, "c");

            bInA.X(4, "bInA");

            cInA.X(5, "cInA");
            cInB.X(6, "cInB");

            Console.ReadLine();
        }
    }

    public class A
    {
        public void X(int lineNum, string label)
        {
            Console.WriteLine(lineNum.ToString() + ". A:X from " + label);
        }
    }

    public class B : A
    {
    }

    public class C : B
    {
    }
}

It produces the following output. The layout is probably a bit confusing, but I hope it will be worth it later when the examples get more complicated. I’ve rearranged things from the linear order of the code into a grid to show the relationship between types and output.

Object type
A B C
Storage type A 1. A:X from a 4. A:X from bInA 5. A:X from cInA
B 2. A:X from b 6. A:X from cInB
C 3. A:X from c

Here comes a diagram explaining what’s going on.  The important thing here is to separate out the lump[s] of code in the bottom row of the diagram from the indexes or pointers into them in the top row.  When something outside the class wants to execute a method inside the class it goes to the relevant class’s index, and from there finds the code to execute.  The indexes are copied down to the derived classes due to inheritance.  As the derived classes don’t override the definition of X, they just point to their immediate parent class’s corresponding index.  C doesn’t point to A; instead it points to B, which in turn points to A.

The case of C shows why the index of a derived class points to its parent’s index rather than its parent’s definition of the method.  For C, its parent (B) has no definition of the method, so all C’s index can do is point to its parent’s index.

In order to stop this article getting even more massive than it already is, I’m showing only C.  I hope that you can prune this down in your head by removing C and B as required, to give you the diagrams for B and A.  The arrows coming down from the top show where to enter the diagram based on what type the storage (the variable) has.  In all cases the definition reached is the same, as there’s only one method definition available.  In later examples there will be different behaviour for the same value type, depending on the type of the storage.

Diagram showing classes A, B and C, with A's definition of X being inherited by B and C

2. Inheriting then overriding

The next example is a small change on the previous one.  B continues to inherit the definition of method X in A, but now C has its own definition.  The new keyword is needed to stop the compiler giving a warning about C hiding the definition in A; the new is short hand for “yes, I really want to hide the definition I’d otherwise inherit”.  Now that there’s sometimes more than one line of output per object I’ve slightly changed the signature of X so that it can group together the output for a single object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    public class A
    {
        public void X(int? lineNum, string label)
        {
            Console.WriteLine((lineNum.HasValue ? lineNum.ToString() + "." : "  ") + " A:X from " + label);
        }
    }

    public class B : A
    {
    }

    public class C : B
    {
        public new void X(int? lineNum, string label)
        {
            Console.WriteLine((lineNum.HasValue ? lineNum.ToString() + "." : "  ") + " C:X from " + label);
            base.X(null, label);
        }
    }
Object type
A B C
Storage type A 1. A:X from a 4. A:X from bInA 5. A:X from cInA
B 2. A:X from b 6. A:X from cInB
C 3. C:X from c
    A:X from c

The code in C writes out its own name and then calls the code in the parent class so that it can write out its name.  The parent class doesn’t have a definition of its own, so the code in C ends up calling the code in A.

Diagram showing C overriding the definition of X from A that's inherited by B.

For instance, with cInA the value is of type C, so the value is made up of A + B + C.  However, calling code accesses it via a variable of type A, which means that it uses the index in A.  This cannot see the definition in C, even though it’s physically there.  (If you were to cast cInA to type C, you would see C’s definition.)  The behaviour of cInA will get a bit weird when we get to virtual methods, but I hope for now it makes sense.  There are two separate concepts – the type of the value (which dictates how much stuff you have) and the type of the variable (which dictates how you interact with that stuff).

3. Filling out the inheritance levels

This might seem like a silly step to take, given that it’s so similar to the previous example, but I don’t want to have to take a big step in terms of code changes to get to the first virtual example, which follows this.  This is to highlight how big the difference is in terms of behaviour.

B now has its own definition of X, which is similar to C‘s.  Given that lots of levels are now logging, I’ve chopped out the details of the logging into a separate class that I’ll show here but then leave out from the later examples for clarity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
   public static class Log
    {
        public static void Write(int? lineNum, string label, string me)
        {
            Console.WriteLine((lineNum.HasValue ? lineNum.ToString() + "." : "  ") + " " + me +" from " + label);
        }
    }

    public class A
    {
        public void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "A:X");
        }
    }

    public class B : A
    {
        public new void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "B:X");
            base.X(null, label);
        }
    }

    public class C : B
    {
        public new void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "C:X");
            base.X(null, label);
        }
    }
Object type
A B C
Storage type A 1. A:X from a 4. A:X from bInA 5. A:X from cInA
B 2. B:X from b
    A:X from b
6. B:X from cInB
    A:X from cInB
C 3. C:X from c
    B:X from c
    A:X from c

Diagram showing A, B and C each with their own definition of X

This continues the pattern from the previous example: bInA has a definition of B.X(), but this is not available so the output is just from A.X().

4. Virtual methods

This example is quite different from the previous example in terms of behaviour, even though the difference in terms of characters on the screen is quite small.  The internals of each method’s definition are the same, but the modifiers before the void are different, and this produces a big difference in the behaviour.  The method in A is now defined as virtual, and the other two methods are defined as override.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public class A
    {
        public virtual void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "A:X");
        }
    }

    public class B : A
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "B:X");
            base.X(null, label);
        }
    }

    public class C : B
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "C:X");
            base.X(null, label);
        }
    }
Object type
A B C
Storage type A 1. A:X from a 4. B:X from bInA
    A:X from bInA
5. C:X from cInA
    B:X from cInA
    A:X from cInA
B 2. B:X from b
    A:X from b
6. C:X from cInB
    B:X from cInB
    A:X from cInB
C 3. C:X from c
    B:X from c
    A:X from c

The three blocks of output 1-3, where each value is held in a variable of the same type, are the same as in the previous example.  The output is different from before when a value is stored in a variable of a different type.

For when bInA.X() is called, the definition in B is found even though bInA is stored in a variable of type A.  Previously, storing anything in A meant that only the definitions in A were accessible.  Similarly, when cInB.X() and cInA.X() are called, the definitions in C are found, even though the variables are of type A and B.

It is tempting, but wrong (as I shall show in later examples) to assume that virtual means that one of the following is happening.  Either:

  1. The index in A and in B are changed to point to the definition in the most derived class present in the value (which would be C for cInA and cInB and B for bInA), or
  2. The indexes stay as they are, but you somehow know to access the method starting at the bottom of the value’s type.

Instead I think that this is what happens with indexes and definitions.

Diagram showing A, B and C being linked into a chain to find the definition for a virtual method

Note that the arrows between classes go in the opposite direction to how they are in the first example, when we had simple inheritance.  As will become clearer in the next example, it’s helpful to think of the arrow being pulled downwards from the parent class to the child class, rather than pushed from the parent to the child.  (It’s a subtle difference, but I’m trying to avoid you heading off one mental path now, and then having to backtrack and pick a different mental path when you get to the next example.)

Also, you’ll notice that the indexes are now split in two for A and B – the classes that have other classes beyond them in the chain of classes linked via the virtual modifier.  The blue bit at the top is what code outside the class hierarchy uses, e.g. the instances in Main.  The white bit at the bottom is what code inside the class hierarchy uses.  So, when C.X() calls base.X(), it goes up to B‘s index and find’s B‘s definition, rather than following the virtual link back to C.  However, when cInA.X() is called (from outside the hierarchy) the look-up starts at A‘s index, then the redirect to B‘s index is followed, and from there the redirect to C‘s index is followed.  As C‘s index isn’t split into internal / external (as there is no class that derives from it), all that’s available there is C‘s definition.

5. Breaking the virtual chain

This example is very similar to the previous one.  The only difference is that there is a break in the chain of virtual methods, by C.X() having the new modifier rather than the override modifier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public class A
    {
        public virtual void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "A:X");
        }
    }

    public class B : A
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "B:X");
            base.X(null, label);
        }
    }

    public class C : B
    {
        public new void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "C:X");
            base.X(null, label);
        }
    }
Object type
A B C
Storage type A 1. A:X from a 4. B:X from bInA
    A:X from bInA
5. B:X from cInA
    A:X from cInA
B 2. B:X from b
    A:X from b
6. B:X from cInB
    A:X from cInB
C 3. C:X from c
    B:X from c
    A:X from c

Diagram similar to the previous one, but C has broken the virtual chain

Note that now that the BC link the virtual chain is broken, the only way for C.X() to be called is to have an object of type C stored in a variable of type C.  The A ⇒ B link in the virtual chain is intact, meaning that B.X() is called as soon as the value has type B, regardless of how it’s stored (as in the previous example).  So the notion “virtual means go to the definition at the bottom of the class hierarchy” isn’t always true – it depends on an unbroken chain from the root class through all derived classes.

I think that the most interesting parts of the output are for cInA and cInB (numbered 5 and 6).  The value is type C, which means that the definitions of A, B and C are all available.  Entering the diagram via storage type A or B produces the same result, because of the A ⇒ B virtual link – the index at A for code outside the class hierarchy redirects to B‘s index rather than to A‘s definition.  At B‘s index we get to the consequences of the B ⇒ C virtual link being broken.  Instead of redirecting onwards to C‘s index, B‘s index points to its own definition.

6. Getting a bit silly

This is the last, and most complicated, example.  Its purpose is to stop you making a wrong assumption about what virtual does.  There are two further levels in the inheritance hierarchy – cunningly named D and E.  They have been added to create enough levels to illustrate some details.  There is a break between B and C: C uses the modifier new rather than override, as in the previous example.  There is then no further break in the section of the class hierarchy CE.  I have also extended the code in Main in a way that I hope you can understand without seeing its listing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    public class A
    {
        public virtual void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "A:X");
        }
    }

    public class B : A
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "B:X");
            base.X(null, label);
        }
    }

    public class C : B
    {
        public new virtual void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "C:X");
            base.X(null, label);
        }
    }

    public class D : C
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "D:X");
            base.X(null, label);
        }
    }

    public class E : D
    {
        public override void X(int? lineNum, string label)
        {
            Log.Write(lineNum, label, "E:X");
            base.X(null, label);
        }
    }
Object type
A B C D E
Storage type A 1. A:X from a 6. B:X from bInA
    A:X from bInA
7. B:X from cInA
    A:X from cInA
9. B:X from dInA
    A:X from dInA
12. B:X from eInA
     A:X from eInA
B 2. B:X from b
    A:X from b
8. B:X from cInB
    A:X from cInB
10. B:X from dInB
     A:X from dInB
13. B:X from eInB
     A:X from eInB
C 3. C:X from c
    B:X from c
    A:X from c
11. D:X from dInC
     C:X from dInC
     B:X from dInC
     A:X from dInC
14. E:X from eInC
     D:X from eInC
     C:X from eInC
     B:X from eInC
     A:X from eInC
D 4. D:X from d
    C:X from d
    B:X from d
    A:X from d
15. E:X from eInD
     D:X from eInD
     C:X from eInD
     B:X from eInD
     A:X from eInD
E 5. E:X from e
    D:X from e
    C:X from e
    B:X from e
    A:X from e

Diagram showing classes A-E, with one virtual chain involving classes A-B and another involving C-E

The top-left corner of the output (numbered 1-3, 6-8) is the same as in the previous example which makes sense because the code is the same in that area.  Because of the break between B and C, following the virtual redirections down from A or B will stop before D or E.  This means that creating a value of type D or E and storing it in something of type A or B won’t lead to D.X() or E.X() being called.

Because the chain is unbroken for the classes C-E, as soon as a value of any of those types is created, the most derived type’s definition is accessed, regardless of the storage’s type (as long as it’s also in the range C-E).

Summary

Well done for reading all this far!  I hope that this has given you a better understanding how virtual methods work in C#.  If anything’s wrong or unclear, please leave a comment below and I’ll try to sort things.

Leave a comment