Polymorphism, Virtual Methods, and Abstract Classes

Polymorphism, Virtual Methods, and Abstract Classes

The Crash Course

  • Polymorphism is a term that means "many forms". In object-oriented programming, polymorphism means that you can create derived classes that implement a method in different ways from each other, and different from what the base class does.
  • A method in a base class can be marked virtual, allowing derived classes to use the override keyword and provide an alternative implementation.
  • People using the base or derived class can call the method that is defined in the base class, and C# will automatically call the appropriate method in the derived class, meaning that the calling code doesn't have to worry about which specific implementation is getting run.
  • Classes can be marked as abstract, making it so you can't actually create an instance of the class. Rather, you'd have to create an instance of a derived class instead.
  • In an abstract class, you can mark methods as abstract as well, and then not provide a method implementation. For derived classes, they will be required to implement the abstract method, or a compile error results.
  • The new keyword, when attached to a method, means that a new method is being created, completely unrelated to any methods in the base class with the same name. This results in two methods with the exact same name, effectively hiding original method in the base class.

Introduction

We've covered the basics of inheritance, but there's still a lot more of the details to go through. In this tutorial, we'll cover most of the more advanced things with inheritance.

We'll start by discussing what polymorphism is, and then discuss how to do it with virtual methods, and overriding methods. We'll then revisit the base keyword one more time, and then we'll take a look at entire classes that are abtract, and can't actually be created—instead, you need to create an instance of a derived class instead. We'll wrap things up by looking at yet another way to use the new keyword, in a way that feels tied to abstract methods and classes, but, it turns out, has nothing to do with it.

Polymorphism: What It Is and Why You Use It

Programmers throw the term "polymorphism" around a lot. It is a fancy Greek word meaning "many forms". Now that we're talking about inheritance and classes, we're to a point where we can look into what polymorphism is, and why it is useful.

Imagine you are making a game, and you have different types of players. One type of player is a human player, which presses the arrow keys to move around on the screen. Another type of player might be a computer controlled player, that also does the same kind of stuff (moving around), but it will figure it out in an entirely different way. For the sake of simplicity, perhaps it just moves around randomly. (Of course, you could go crazy with fancy artificial intelligence path finding algorithms and what not, but that's a whole other discussion for another day. For now, we'll stick with the random AI player.)

We could create a Player class, which has a method called MakeMove(), that returns a direction to move. Then, using what we know about inheritance, we could create a derived class called HumanPlayer that handles its move making by checking what keys the user presses, while a RandomAIPlayer could make its decisions completely differently—picking a random direction to go.

What we're describing here is the root of polymorphism. We have a base class, Player, that knows it needs to be able to make a move somehow, but as far as we're concerned, it doesn't matter how, specifically, and other derived classes, the HumanPlayer and the RandomAIPlayer that want to be able to implement the method using whatever techniques they feel are needed. The two derived classes can have two entirely unrelated ways of doing this, so there's multiple "forms" for accomplishing it. This is what polymorphism is.

And from outside of these classes, if we have a Player object, we can call MakeMove(), and the player will find out how to make a move, using whatever it wants, and we get a result back. From this point of view, it doesn't matter how it was decided, just that it was decided.

C# allows us to do polymorphism in a very easy manner.

Virtual Methods and Overriding

Let's start with creating an example of the Player class that we were discussing:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Inheritance
{
    public enum MoveDirection { None, Left, Right, Up, Down };
 
    class Player
    {
        public virtual MoveDirection MakeMove()
        {
            return MoveDirection.Left;
        }
    }
}

Here, in this code, we define a MoveDirection enum, which defines the directions the player can move. We then have our player class, which is made just like any other class. We define our MakeMove() method, which uses the enumeration we created as its return type, and add in the virtual keyword.

The virtual keyword means that derived classes will be able to change the way this method is defined, providing their own definition. This will allow us to do polymorphism.

Now, let's look at the HumanPlayer class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Inheritance
{
    class HumanPlayer : Player
    {
        public HumanPlayer()
        {
        }
 
        public override MoveDirection MakeMove()
        {
            ConsoleKeyInfo info = Console.ReadKey();
 
            if(info.Key == ConsoleKey.LeftArrow) { return MoveDirection.Left; }
            if(info.Key == ConsoleKey.RightArrow) { return MoveDirection.Right; }
            if(info.Key == ConsoleKey.UpArrow) { return MoveDirection.Up; }
            if (info.Key == ConsoleKey.DownArrow) { return MoveDirection.Down; }
 
            return MoveDirection.None;
        }
    }
}

Here, we create a class like normal, deriving from the Player class like we've seen before, and we create the MakeMove() method here, doing whatever we want to have this method do for the HumanPlayer class.

We do this by adding the override keyword to the method.

Now, if we are working with a Player object that happens to be a HumanPlayer, and we call MakeMove(), the new code, here, which reads input from the keyboard will be executed instead of anything we put in the base Player class. This new method definition in the HumanPlayer class "overrides" the definition in the base class.

We can also create our RandomAIPlayer class in a similar way, implementing the MakeMove() method differently:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Inheritance
{
    class RandomAIPlayer : Player
    {
        private Random random;
 
        public RandomAIPlayer()
        {
            random = new Random();
        }
 
        public override MoveDirection MakeMove()
        {
            int choice = random.Next(4);
 
            if (choice == 0) { return MoveDirection.Left; }
            if (choice == 1) { return MoveDirection.Right; }
            if (choice == 2) { return MoveDirection.Up; }
            if (choice == 3) { return MoveDirection.Down; }
 
            return MoveDirection.None;
        }
    }
}

To fully illustrate how this works, let's look at some code that could go elsewhere in your program to use this:

Player player1 = new HumanPlayer();
Player player2 = new RandomAIPlayer();
 
MoveDirection player1Direction = player1.MakeMove();
MoveDirection player2Direction = player2.MakeMove();

At this point in the code, we're not concerned with how the player made its decision on where to move, just that it made a move, and we can make the game progress. Each individual type of player can figure out the details on their own, allowing the code that uses it to focus on other things.

Note that in this particular case, where the base Player class provides a definition for this method, the derived class doesn't actually need to override the method. Without overriding it, the version that is in the base class would be used instead. So, with the way we've done it so far, there's no need to override a method, but we are allowed to override the method.

Revisiting the base Keyword

Let's take a second look, now, at the base keyword. Let's say you are in a derived class, and you are overriding a particular method. The base class provided some type of default implementation for the method, which is valid in most cases, but in some particular case, you want to do something different. In this case, you can override the method.

But what if the base class's implementation has done a lot of useful work, that you'd like to reuse? In any method, you can use the base keyword and access the methods of the base class:

public override MoveDirection MakeMove()
{
    MoveDirection moveDirection = base.MakeMove();
    if(moveDirection == MoveDirection.None)
    {
        return MoveDirection.Up;
    }
}

Using the base keyword in a situation like this allows us to still have access to the original method implementation, while still overriding the method to do something different.

Abstract Base Classes

Let's go back to our Player example. The Player class defines what a player can do, but in reality, in this specific case, we may not want anyone to actually be able to make just a Player. We'd always want them to make a HumanPlayer, or a RandomAIPlayer, or potentially a different type that we haven't created here.

It is possible to make sure that you can't actually create an instance of a specific type of class, like we might want to do with the Player class here, by marking it with the abstract keyword:

abstract class Player
{
    public virtual MoveDirection MakeMove();
}

Adding the abstract keyword ensures that people can't create just a player. (In other words, you can't use Player player = new Player();. You have to use Player player = new HumanPlayer(); or Player player = new RandomAIPlayer();.

Abstract Methods

Still looking at our example, we have defined a Player class that has a MakeMove() method, but we've really just put this in because whenever we make a method, we need to tell it what it does—we need to implement it.

If we have an abstract base class (marked with the abstract keyword) though, we may have some methods that make no sense to implement, like our MakeMove() method here, or that instead of providing a "default" definition, we want to require any derived class to make their own implementation.

Inside of an abstract class, you can also attach the abstract keyword to any method you want, and not provide a definition. This works in a ways similar to the virtual keyword, only instead of providing a method implementation, we can just end the method declaration with a semicolon:

public abstract MoveDirection MakeMove();

Now, we don't need to provide a default definition in the Player class at all. Instead, the derived classes, HumanPlayer and RandomAIPlayer will need to provide their own implementation of the method instead.

The new Keyword with Methods

There's one other topic that a lot of people seem to get tangled up in the override and virtual keywords for methods, and that is the new keyword.

You are allowed to use the new keyword on a method like this:

public new MoveDirection MakeMove()
{
    //...
}

When you use the new keyword here, what you are saying is, "I'm making a brand new method in the derived class that has absolutely nothing to do with the method with the same name in the base class." So here, this MakeMove method is completely unrelated to the MakeMove method in the Player class, which, to be honest, is probably a bad idea in most cases. Now you'd have two methods floating around with the same name and signature, but the two aren't related at all.

People often get this new keyword confused with overriding virtual and abstract methods, probably because of two things. One, it looks similar. The new keyword here, gets used in the exact same way as override would. In addition, if you forget to put anything in at all (new or override) when something is supposed to be there, the C# compiler will give you a warning that says something to the effect of "you need to either use the override keyword here, or the new keyword", and in fact, by default, if you leave something off, it does the same thing that new does.

What's Next?

Inheritance, polymorphism, virtual methods, and abstract base classes can be one of the trickiest things to grasp about object-oriented programming and C#. Don't panic if you didn't understand everything that was discussed here. You can come back and re-read it whenever you want, and the reality is, it may only really sink in as you use it more and more in your own programming.

We're going to finish up our discussion about object-oriented programming next by discussing interfaces. After that, we'll be move on and discuss kind of a random collection of assorted other important topics in C#, before finishing up the C# Crash Course.