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 also mark methods as abstract and then not provide a method implementation. For derived classes, they will be required to implement the abstract method.
- 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 the original method in the base class.
Introduction
We've covered the basics of inheritance, but there are still a lot more details to go through. This tutorial will 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 by overriding methods. We'll revisit the base keyword one more time, and then we'll look at entire classes that are abstract 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 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 does the same kind of stuff (moving around), but it will figure it out entirely differently. 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 whatnot, 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 are 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:
public enum MoveDirection { None, Left, Right, Up, Down }; public class Player { public virtual MoveDirection MakeMove() { return MoveDirection.Left; // The default is to always move 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:
class HumanPlayer : Player { 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 as 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:
class RandomAIPlayer : Player { private Random _random; public RandomAIPlayer() { _random = new Random(); } public override MoveDirection MakeMove() { // Switch expressions and enumerations often appear together! return _random.Next(4) switch { 0 => MoveDirection.Left, 1 => MoveDirection.Right, 2 => Move Direction.Up, _ => MoveDirection.Down }; } }
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 case, where the base Player class provides a definition for this method, the derived class doesn't 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 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 cases, 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), we may have some methods that make no sense to implement in the abstract class. We may want to demand that derived classes provide an implementation without providing one down at the abstract base class level. Our MakeMove() method is a good example of this. Player's implementation is utter garbage. We don't really want anybody accidentally using that. We'd rather people just provide an actual, correct implementation of it instead.
Inside an abstract class, you can attach the abstract keyword to any method you want and not provide a definition. This works in a way 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() { //... }
This tells the compiler, "I'm making a totally new method. Even though it has the same name as something in the base class, it is not meant to be an override. It is its own thing."
This should be extremely rare. Adding methods with the same name but with different meanings is just going to be confusing. The main reason I bring it up is that most of the compiler errors that happen when you accidentally leave out override suggest new as a potential solution. It sometimes is, but really, that is almost never what you want. You usually just forgot to add override (and maybe virtual or abstract in the base class).
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 move on and discuss a random collection of other important topics in C# before finishing up the C# Crash Course.
Post preview:
Close preview