6 min read

Polymorphism, Virtual Methods, and Abstract Classes

Crash Course

Introduction

In this tutorial, we’ll strap a jetpack on the concept of inheritance that we saw in the previous tutorial and look at, perhaps, the key feature that makes inheritance (and maybe all of object-oriented programming) so powerful.

In the previous tutorial, we saw that we can define one class that extends or builds upon another class through inheritance. With inheritance, you can augment what was already there, but don’t have any ability to change or substitute the behavior in the base class. In this tutorial, we’ll cover the fundamentals of the C# tools that allow us to do these modifications.

Imagine you are building a game where characters are moved around on a grid by players. You want to have human players as well as computer (AI) players. You can imagine making a class to represent human players, entering key strokes on the keyboard and a class to represent AI players that use other logic to decide how to move. You can also imagine making a class that represents any player, a bit more abstractly. This could be a base class to our human player and AI player classes, so we can start with the following partial solution:

class Player
{
}

class HumanPlayer : Player
{

}

class RandomAIPlayer : Player
{

}

This would let us set up a new game with a human player vs. a computer player:

Player player1 = new HumanPlayer();
Player player2 = new RandomAIPlayer();

But the next step is to come up with a way to ask both players to pick a direction to travel on their turn. (Or if we’re making a real-time game instead of a turn-based game, it could be that we just ask for the desired direction periodically.)

So perhaps we add in this enumeration:

enum Direction { Up, Down, Left, Right }

But given only the tools we know, it is tricky to figure out how we’d let a human player make moves based on keyboard input, while letting the AI player use other logic to make a move.

The best we could do is this:

class Player
{
}

class HumanPlayer
{
    public Direction MakeHumanMove()
    {
        ConsoleKey key = Console.ReadKey().KeyCode;

        return key switch
        {
            ConsoleKey.LeftArrow => Direction.Left,
            ConsoleKey.RightArrow => Direction.Right,
            ConsoleKey.UpArrow => Direction.Up,
            ConsoleKey.DownArrow => Direction.Down
        };
    }
}

class RandomAIPlayer
{
    public Direction MakeComputerMove()
    {
        Random random = new Random();

        int choice =- random.Next(4);

        return choice switch
        {
            0 => Direction.Left,
            1 => Direction.Right,
            2 => Direction.Up,
            2 => Direction.Down
        };
    }
}

Before we move on, I want to say that there’s a lot of code there, and these tutorials haven’t explained everything in it. There’s some new stuff there. The details don’t matter as much as the concepts. But take a few minutes to see how the human player uses the ReadKey method and the ConsoleKey enumeration to pick a direction, and how the RandomAIPlayer class uses an instance of the Random class to pick a random direction. (Note that the Next(4) part will pick a random number between 0 and 3–four total choices, but starting at 0.)

From a perspective of inheritance and where we’re going with this tutorial, the important thing to point out is that the one class has a MakeHumanMove method, while the other has a MakeComputerMove method. That means that we need to do something like this to use our two players:

Player player1 = new HumanPlayer();
Player player2 = new RandomAIPlayer();

// Later...
Direction direction;
if (player1 is HumanPlayer human) direction = human.MakeHumanMove();
else if (player1 is RandomAIPlayer computer) direction = computer.MakeComputerMove();

// Repeat for Player 2.

Now before you say, “Well you should have just named the two methods the same thing!”, note that even if they had both been called MakeMove, the Player class itself has no concept of a MakeMove method. We’d still have to do the same thing as above, we’d just be using different names.

But this is where polymorphism comes in. Polymorphism is a rather simple concept with a rather complicated name. The idea is that a base class defines a method, but derived classes like HumanPlayer and RandomAIPlayer can supply their own definition for it, that performs the task in a way that is unique to that type. Thus, there will be a MakeMove concept defined at the Player base class level, but when it is called, the correct human or computer version will actually run, depending on the object’s actual type (not the variable’s type).

Adding Polymorphism

We start by adding a definition for a method in the base class. This looks like a normal method, but includes the virtual keyword:

class Player
{
    public virtual Direction MakeMove()
    {
        return Direction.Up;
    }
}

The virtual keyword signals that this is a method that derived classes have the option to change, by supplying their own implementation.

Then in the derived classes, you define the method using the same name, return type, and parameters, but include the override keyword:

class HumanPlayer
{
    public override Direction MakeMove()
    {
        ConsoleKey key = Console.ReadKey().KeyCode;

        return key switch
        {
            ConsoleKey.LeftArrow => Direction.Left,
            ConsoleKey.RightArrow => Direction.Right,
            ConsoleKey.UpArrow => Direction.Up,
            ConsoleKey.DownArrow => Direction.Down
        };
    }
}

You’d do a similar thing for the RandomAIPlayer class.

Now we can write code like this:

Player player1 = new HumanPlayer();
Player player2 = new RandomAIPlayer();

Direction direction = player1.MakeMove();

And in case that isn’t clear enough, the following also works:

Player player1 = MakeAPlayerBasedOnUserChoice();
Player player2 = MakeAPlayerBasedOnUserChoice();

Direction direction = player1.MakeMove();

It doesn’t matter if Player contains an instance of a human player or a computer player. We can call the MakeMove method on it, and depending on which type it actually is, it will run the appropriate method.

note

If a base class defines a method, and a derived class does not override it, then the version defined in the base class will be used. Therefore, overriding a method in a derived class is purely optional, if the base class defines the method, as we’ve seen above.

Abstract Methods and Classes

Making a virtual method and overriding it in derived classes works well when there’s some good default behavior to use. That default behavior can be defined in the base class, and only classes that need something else are required to override it.

When there is no reasonable default behavior, another option is to indicate that the method must exist, but not provide a body for it. This is called an abstract method and requires using the abstract keyword:

abstract class Player
{
    public abstract Direction MakeMove();
}

Derived classes override an abstract method in exactly the same way as a virtual method. But the difference is that, while a virtual method can be overridden, an abstract method must be overridden.

Note also that the abstract keyword was applied on the first line, to the class as a whole.

Any class that contains an abstract method must be made abstract itself. (Though you are allowed to make abstract classes that don’t have any abstract methods, if you want.)

The base Keyword

Sometimes, the base class provides meaningful behavior that you just want to augment in your derived class. By using the base keyword, you can call the base class’s version of a method from a derived class. In our example earlier, this isn’t very practical, because our Player base class wasn’t doing anything smart. But to illustrate the mechanics:

public override Direction MakeMove()
{
    Random random = new Random();

    // 50% of the time, just do whatever the base class said. The rest of the time,
    // continue on and pick a random direction.
    if (random.Next(2) == 0)
        return base.MakeMove();

    int choice =- random.Next(4);

    return choice switch
    {
        0 => Direction.Left,
        1 => Direction.Right,
        2 => Direction.Up,
        2 => Direction.Down
    };
}