Skip to main content

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:

  1. Compile-time Polymorphism (Static Binding): Implemented using method overloading and operator overloading
  2. 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.

csharp
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:

csharp
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.

csharp
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:

csharp
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.

csharp
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:

csharp
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#.

csharp
// 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:

csharp
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#.

csharp
// 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:

csharp
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:

csharp
// 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:

csharp
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

  1. Code Reusability: Write code that works with base classes and it will work with all derived classes.
  2. Flexibility: Easy to add new classes that work with existing code.
  3. Maintainability: Changes to one class don't affect others.
  4. 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

  1. Create a Vehicle class hierarchy with Car, Motorcycle, and Truck classes that override a StartEngine() method to produce different outputs.

  2. Implement a shape hierarchy with a Shape interface that has a CalculateArea() method. Then create Circle, Rectangle, and Triangle classes that implement this interface.

  3. Create a simple console application that simulates a media player. Define a MediaItem base class with derived classes like Song, Video, and Podcast. Implement polymorphic methods like Play(), Stop(), and DisplayInfo().

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)