C# Polymorphism

One interface, multiple implementations

🎭 What is Polymorphism?

Polymorphism means "many forms." It allows objects of different classes to be treated as objects of a common base class, enabling one interface to represent different underlying forms.


class Animal {
    public virtual void MakeSound() {
        Console.WriteLine("Some sound");
    }
}

class Cat : Animal {
    public override void MakeSound() {
        Console.WriteLine("Meow!");
    }
}

class Dog : Animal {
    public override void MakeSound() {
        Console.WriteLine("Woof!");
    }
}
                                    

Output:

Meow!

Woof!

Types of Polymorphism

⏱️

Compile-Time

Method overloading

void Print(int x) { }
void Print(string x) { }
🏃

Runtime

Method overriding

public override 
void Display() { }
🔀

Virtual Methods

Can be overridden

public virtual 
void Calculate() { }
🎯

Abstract Methods

Must be overridden

public abstract 
void Process();

🔹 Method Overloading (Compile-Time)

Method overloading allows multiple methods with the same name but different parameter lists within the same class. The compiler selects the appropriate method at compile-time based on the number, type, and order of arguments. This technique enhances API clarity and usability by providing intuitive, context-specific method signatures without cluttering the namespace with different names for similar operations.

class Calculator {
    // Same method name, different parameters
    public int Add(int a, int b) {
        return a + b;
    }
    
    public double Add(double a, double b) {
        return a + b;
    }
    
    public int Add(int a, int b, int c) {
        return a + b + c;
    }
}

// Usage
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(5, 3));           // Calls int version
Console.WriteLine(calc.Add(5.5, 3.2));       // Calls double version
Console.WriteLine(calc.Add(1, 2, 3));        // Calls three-parameter version

Output:

8

8.7

6

🔹 Method Overriding (Runtime)

Method overriding enables a derived class to provide a specific implementation for a method already defined in its base class. The decision of which method to execute is made at runtime based on the object's type. This is a key aspect of runtime polymorphism, allowing for dynamic behavior and extensible design where subclass behavior can replace or enhance inherited functionality.

class Vehicle {
    public virtual void Start() {
        Console.WriteLine("Vehicle is starting");
    }
}

class Car : Vehicle {
    public override void Start() {
        Console.WriteLine("Car engine is starting");
    }
}

class Motorcycle : Vehicle {
    public override void Start() {
        Console.WriteLine("Motorcycle is kick-starting");
    }
}

// Usage - Runtime polymorphism
Vehicle vehicle1 = new Car();
Vehicle vehicle2 = new Motorcycle();
vehicle1.Start();
vehicle2.Start();

Output:

Car engine is starting

Motorcycle is kick-starting

🔹 Polymorphism with Arrays

Polymorphism is particularly powerful when used with collections, such as arrays, that hold references to a base class type. You can store objects of various derived types in the same array and invoke overridden methods, with each object executing its own implementation. This approach simplifies code that works with heterogeneous objects, like rendering shapes or processing different document types through a common interface.

class Shape {
    public virtual double GetArea() {
        return 0;
    }
}

class Circle : Shape {
    public double Radius = 5;
    
    public override double GetArea() {
        return 3.14 * Radius * Radius;
    }
}

class Rectangle : Shape {
    public double Width = 4;
    public double Height = 6;
    
    public override double GetArea() {
        return Width * Height;
    }
}

// Usage - Array of different shapes
Shape[] shapes = new Shape[2];
shapes[0] = new Circle();
shapes[1] = new Rectangle();

foreach (Shape shape in shapes) {
    Console.WriteLine($"Area: {shape.GetArea()}");
}

Output:

Area: 78.5

Area: 24

🔹 Virtual vs Override Keywords

The virtual keyword marks a method in a base class as overridable, while override is used in a derived class to provide a new implementation. This mechanism is central to achieving runtime polymorphism. Without virtual, a method is not eligible for overriding. The override modifier ensures type safety and clarity, making the developer's intent explicit and the behavior predictable during inheritance hierarchies.

class Employee {
    public string Name;
    
    public virtual double CalculateSalary() {
        return 50000;  // Base salary
    }
}

class Manager : Employee {
    public override double CalculateSalary() {
        return 80000;  // Manager salary
    }
}

class Developer : Employee {
    public override double CalculateSalary() {
        return 70000;  // Developer salary
    }
}

// Usage
Employee emp1 = new Manager();
Employee emp2 = new Developer();
Console.WriteLine($"Manager: ${emp1.CalculateSalary()}");
Console.WriteLine($"Developer: ${emp2.CalculateSalary()}");

Output:

Manager: $80000

Developer: $70000

🧠 Test Your Knowledge

What type of polymorphism is method overloading?