Understanding Polymorphism in C# with Examples
Polymorphism is a core concept of object-oriented programming that allows objects of different classes to be treated as objects of a common superclass or interface. This article explains the fundamental principles of polymorphism in C#, breaking down the differences between compile-time (static) polymorphism and run-time (dynamic) polymorphism. Through clear, straightforward code examples, you will learn how to implement method overloading, method overriding, and interface-based polymorphism to write flexible and maintainable code.
What Is Polymorphism?
The word "polymorphism" is derived from Greek, meaning "many forms." In C#, it refers to the ability of a single interface, method, or class to take on different behaviors depending on the context in which it is invoked.
Polymorphism helps eliminate repetitive code, decouples class implementations from consumer logic, and enables code extensibility without modifying existing systems.
C# primarily supports two types of polymorphism:
- Compile-Time Polymorphism (Static Binding)
- Run-Time Polymorphism (Dynamic Binding)
1. Compile-Time Polymorphism (Method Overloading)
Compile-time polymorphism occurs when the compiler determines which method to execute before the program runs. The most common implementation is method overloading, where multiple methods in the same class share the same name but differ in their parameters (by count, type, or order).
Example:
public class Calculator
{
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;
}
}When you call calculator.Add(5, 10), the compiler
selects the first version. If you pass 5.5, 2.3, it
resolves to the second version.
2. Run-Time Polymorphism (Method Overriding)
Run-time polymorphism occurs when the method call is resolved at
runtime based on the actual object type, rather than the reference type.
In C#, this is achieved using the virtual and
override keywords with inheritance.
virtual: Declared in the base class to indicate that a method can be replaced by a derived class.override: Declared in the derived class to provide a new implementation of the base class's virtual method.
Example:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("The cat meows.");
}
}Usage:
List<Animal> animals = new List<Animal>
{
new Dog(),
new Cat(),
new Animal()
};
foreach (Animal animal in animals)
{
// The specific method is determined dynamically at runtime
animal.MakeSound();
}Output:
The dog barks.
The cat meows.
The animal makes a sound.
Even though the collection is typed as Animal, C# calls
the overridden version specific to each object instance.
3. Interface-Based Polymorphism
Interfaces provide another way to achieve run-time polymorphism without requiring a shared class hierarchy. An interface defines a contract that implementing classes must follow.
Example:
public interface IShape
{
double CalculateArea();
}
public class Circle : IShape
{
public double Radius { get; set; }
public Circle(double radius) => Radius = radius;
public double CalculateArea() => Math.PI * Radius * Radius;
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public double CalculateArea() => Width * Height;
}Usage:
IShape shape1 = new Circle(5);
IShape shape2 = new Rectangle(4, 6);
Console.WriteLine($"Circle Area: {shape1.CalculateArea()}");
Console.WriteLine($"Rectangle Area: {shape2.CalculateArea()}");Summary
Polymorphism in C# allows developers to design flexible systems where specific behaviors are decoupled from general contracts. Use compile-time polymorphism (overloading) when actions perform similarly with different parameter inputs, and run-time polymorphism (overriding and interfaces) when distinct classes must provide specialized responses to a unified command.