C# Polymorphism
Introduction
Polymorphism is one of the four fundamental pillars of Object-Oriented Programming (OOP), alongside encapsulation, inheritance, and abstraction. The word "polymorphism" comes from Greek words meaning "many forms" - and that's exactly what it enables in your code: the ability for objects to take on many forms.
In C#, polymorphism allows you to work with objects of different classes through a common interface. This means you can write code that works with a base class, but at runtime, that code can work with any class that inherits from that base class. This makes your code more flexible, reusable, and easier to maintain.
In this tutorial, we'll explore the different types of polymorphism in C#, how to implement them, and why they're useful in real-world applications.
Types of Polymorphism in C#
C# supports two main types of polymorphism:
- Compile-time Polymorphism (Static Binding): Implemented using method overloading and operator overloading
- Run-time Polymorphism (Dynamic Binding): Implemented using method overriding
Let's explore each of these in detail.
Compile-time Polymorphism
Also known as static polymorphism, this type is resolved during compile time. It includes:
Method Overloading
Method overloading allows you to define multiple methods with the same name but with different parameters.
class Calculator
{
// Method with two integer parameters
public int Add(int a, int b)
{
return a + b;
}
// Method with three integer parameters
public int Add(int a, int b, int c)
{
return a + b + c;
}
// Method with two double parameters
public double Add(double a, double b)
{
return a + b;
}
}
Here's how you can use these overloaded methods:
class Program
{
static void Main()
{
Calculator calc = new Calculator();
Console.WriteLine($"Adding two integers: {calc.Add(5, 10)}");
Console.WriteLine($"Adding three integers: {calc.Add(5, 10, 15)}");
Console.WriteLine($"Adding two doubles: {calc.Add(5.5, 10.5)}");
}
}
Output:
Adding two integers: 15
Adding three integers: 30
Adding two doubles: 16
The compiler determines which version of the Add
method to call based on the number and types of arguments provided.
Operator Overloading
C# also allows you to overload certain operators for your custom classes.
class Vector
{
public int X { get; set; }
public int Y { get; set; }
public Vector(int x, int y)
{
X = x;
Y = y;
}
// Overloading the + operator
public static Vector operator +(Vector v1, Vector v2)
{
return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
Using the overloaded operator:
class Program
{
static void Main()
{
Vector v1 = new Vector(2, 3);
Vector v2 = new Vector(1, 4);
Vector v3 = v1 + v2; // Using the overloaded + operator
Console.WriteLine($"v1: {v1}");
Console.WriteLine($"v2: {v2}");
Console.WriteLine($"v1 + v2: {v3}");
}
}
Output:
v1: (2, 3)
v2: (1, 4)
v1 + v2: (3, 7)
Runtime Polymorphism
Runtime polymorphism, also known as dynamic polymorphism, is resolved during runtime and is implemented using method overriding.
Method Overriding
Method overriding allows a derived class to provide a specific implementation of a method that is already defined in its base class.
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Dog barks: Woof! Woof!");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Cat meows: Meow! Meow!");
}
}
Using the overridden methods:
class Program
{
static void Main()
{
// Create an array of Animal objects
Animal[] animals = new Animal[3];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();
// Loop through the array and call MakeSound()
foreach (Animal animal in animals)
{
animal.MakeSound(); // Polymorphic call
}
}
}
Output:
Animal makes a sound
Dog barks: Woof! Woof!
Cat meows: Meow! Meow!
Even though we're using the Animal
type to refer to each object, the correct version of the MakeSound()
method is called based on the actual object type at runtime. This is the essence of polymorphism.
Abstract Classes and Methods
Abstract classes and methods are important tools for achieving polymorphism in C#.
// Abstract class
abstract class Shape
{
// Abstract method (must be implemented by derived classes)
public abstract double CalculateArea();
// Regular method
public void DisplayArea()
{
Console.WriteLine($"Area: {CalculateArea()}");
}
}
// Concrete classes implementing the abstract class
class Circle : Shape
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
public override double CalculateArea()
{
return Math.PI * radius * radius;
}
}
class Rectangle : Shape
{
private double width;
private double height;
public Rectangle(double width, double height)
{
this.width = width;
this.height = height;
}
public override double CalculateArea()
{
return width * height;
}
}
Using these classes:
class Program
{
static void Main()
{
// Cannot instantiate an abstract class
// Shape shape = new Shape(); // This would cause a compilation error
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
circle.DisplayArea();
rectangle.DisplayArea();
}
}
Output:
Area: 78.53981633974483
Area: 24
Interfaces and Polymorphism
Interfaces are another powerful way to implement polymorphism in C#.
// Define an interface
interface IDrawable
{
void Draw();
}
// Classes implementing the interface
class Circle : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a circle");
}
}
class Square : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a square");
}
}
class Line : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a line");
}
}
Using these implementations:
class Program
{
static void Main()
{
// Create a list of IDrawable objects
List<IDrawable> shapes = new List<IDrawable>
{
new Circle(),
new Square(),
new Line()
};
// Draw all shapes
foreach (IDrawable shape in shapes)
{
shape.Draw(); // Polymorphic call
}
}
}
Output:
Drawing a circle
Drawing a square
Drawing a line
Real-World Example: Game Characters
Let's see a more complex real-world example of how polymorphism can be used in a game to handle different types of characters:
// Base class
abstract class GameCharacter
{
public string Name { get; set; }
public int Health { get; set; }
public int Power { get; set; }
public GameCharacter(string name, int health, int power)
{
Name = name;
Health = health;
Power = power;
}
// Abstract methods
public abstract void Attack();
public abstract void SpecialMove();
// Virtual method
public virtual void Defend()
{
Console.WriteLine($"{Name} is defending. Reduced damage taken.");
}
public void DisplayStatus()
{
Console.WriteLine($"Character: {Name} | Health: {Health} | Power: {Power}");
}
}
// Derived classes
class Warrior : GameCharacter
{
public Warrior(string name) : base(name, 200, 50) { }
public override void Attack()
{
Console.WriteLine($"{Name} swings a sword with {Power} power!");
}
public override void SpecialMove()
{
Console.WriteLine($"{Name} performs a whirlwind attack dealing damage to all enemies!");
}
// Overriding the virtual method
public override void Defend()
{
Console.WriteLine($"{Name} raises a shield, blocking most damage!");
base.Defend(); // Calling the base implementation
}
}
class Mage : GameCharacter
{
public int ManaPoints { get; set; }
public Mage(string name) : base(name, 100, 100)
{
ManaPoints = 200;
}
public override void Attack()
{
Console.WriteLine($"{Name} casts a fireball with {Power} power!");
}
public override void SpecialMove()
{
Console.WriteLine($"{Name} summons a meteor storm dealing massive area damage!");
ManaPoints -= 50;
}
}
class Archer : GameCharacter
{
public int Arrows { get; set; }
public Archer(string name) : base(name, 150, 70)
{
Arrows = 50;
}
public override void Attack()
{
if (Arrows > 0)
{
Console.WriteLine($"{Name} shoots an arrow with {Power} power!");
Arrows--;
}
else
{
Console.WriteLine($"{Name} is out of arrows! Can't attack!");
}
}
public override void SpecialMove()
{
if (Arrows >= 5)
{
Console.WriteLine($"{Name} performs a rapid fire, shooting 5 arrows at once!");
Arrows -= 5;
}
else
{
Console.WriteLine($"{Name} doesn't have enough arrows for a special move!");
}
}
}
Now let's use these characters in a game scenario:
class Program
{
static void Main()
{
// Create different game characters
List<GameCharacter> characters = new List<GameCharacter>
{
new Warrior("Conan"),
new Mage("Gandalf"),
new Archer("Legolas")
};
// Game loop
Console.WriteLine("=== GAME CHARACTERS ===");
foreach (GameCharacter character in characters)
{
character.DisplayStatus();
character.Attack();
character.SpecialMove();
character.Defend();
Console.WriteLine(); // Add a blank line for readability
}
// Another example - character selection
Console.WriteLine("=== CHARACTER SELECTION ===");
Console.WriteLine("Select a character:");
for (int i = 0; i < characters.Count; i++)
{
Console.WriteLine($"{i+1}. {characters[i].Name}");
}
// Let's say the player selected the Mage (index 1)
GameCharacter selectedCharacter = characters[1];
Console.WriteLine($"\nYou selected {selectedCharacter.Name}!");
// Even though selectedCharacter is of type GameCharacter,
// the correct implementations of the methods are called
selectedCharacter.Attack();
selectedCharacter.SpecialMove();
}
}
Output:
=== GAME CHARACTERS ===
Character: Conan | Health: 200 | Power: 50
Conan swings a sword with 50 power!
Conan performs a whirlwind attack dealing damage to all enemies!
Conan raises a shield, blocking most damage!
Conan is defending. Reduced damage taken.
Character: Gandalf | Health: 100 | Power: 100
Gandalf casts a fireball with 100 power!
Gandalf summons a meteor storm dealing massive area damage!
Gandalf is defending. Reduced damage taken.
Character: Legolas | Health: 150 | Power: 70
Legolas shoots an arrow with 70 power!
Legolas performs a rapid fire, shooting 5 arrows at once!
Legolas is defending. Reduced damage taken.
=== CHARACTER SELECTION ===
Select a character:
1. Conan
2. Gandalf
3. Legolas
You selected Gandalf!
Gandalf casts a fireball with 100 power!
Gandalf summons a meteor storm dealing massive area damage!
This example shows how polymorphism allows us to treat different types of game characters uniformly through their common base class, while still allowing each character to implement its unique behaviors.
Benefits of Polymorphism
- Code Reusability: Write code that works with base classes and it will work with all derived classes.
- Flexibility: Easy to add new classes that work with existing code.
- Maintainability: Changes to one class don't affect others.
- Extensibility: Systems can be easily extended with new functionality.
Summary
Polymorphism is a powerful concept in C# and object-oriented programming that allows objects of different classes to be treated as objects of a common base type. We've explored:
- Compile-time polymorphism through method overloading and operator overloading
- Runtime polymorphism through method overriding
- Abstract classes and how they facilitate polymorphic behavior
- Interfaces as another way to achieve polymorphism
- Real-world examples showing polymorphism in action
By mastering polymorphism, you can write more flexible, maintainable, and extensible code.
Exercises
-
Create a
Vehicle
class hierarchy withCar
,Motorcycle
, andTruck
classes that override aStartEngine()
method to produce different outputs. -
Implement a shape hierarchy with a
Shape
interface that has aCalculateArea()
method. Then createCircle
,Rectangle
, andTriangle
classes that implement this interface. -
Create a simple console application that simulates a media player. Define a
MediaItem
base class with derived classes likeSong
,Video
, andPodcast
. Implement polymorphic methods likePlay()
,Stop()
, andDisplayInfo()
.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)