Polymorphism Concepts
Vaibhav • September 10, 2025
In the previous articles, we explored how interfaces define contracts and how inheritance allows classes to share and override behavior. Now we arrive at one of the most powerful and elegant ideas in object-oriented programming: polymorphism. The word itself means “many forms,” and in C#, polymorphism allows objects to behave differently depending on their runtime type - even when accessed through a common interface or base class. This article will unpack what polymorphism is, how it works in practice, and how to design with it effectively.
What is polymorphism?
Polymorphism is the ability to treat different types of objects through a shared interface or base class, and have each object respond in its own way. It’s what allows you to write code that works with general types, while still benefiting from specific behavior at runtime.
interface IShape
{
double Area();
}
class Circle : IShape
{
public double Radius;
public Circle(double radius) { Radius = radius; }
public double Area() => Math.PI * Radius * Radius;
}
class Square : IShape
{
public double Side;
public Square(double side) { Side = side; }
public double Area() => Side * Side;
}
Both Circle
and Square
implement IShape
. You can now treat them as IShape
objects
and call Area()
without knowing the exact type.
List shapes = new List
{
new Circle(3),
new Square(4)
};
foreach (IShape shape in shapes)
{
Console.WriteLine($"Area: {shape.Area()}");
}
This loop prints the area of each shape, even though the actual calculation differs. That’s polymorphism in action - the same method call behaves differently depending on the object’s type.
Polymorphism works through method overriding (in class hierarchies) or interface implementation. The runtime uses the actual object type to decide which method to call.
Polymorphism with base classes
You can also achieve polymorphism using base classes and virtual methods. This is common when you want to provide default behavior that derived classes can override.
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
Here, Speak()
is a virtual method in the base class Animal
. Each derived class overrides it to provide its own sound. You can now
treat all animals uniformly:
List animals = new List
{
new Dog(),
new Cat()
};
foreach (Animal animal in animals)
{
animal.Speak();
}
Even though the list is typed as Animal
, each call to Speak()
invokes the correct override. This is runtime polymorphism.
Virtual, override, and base keywords
To enable polymorphism in class hierarchies, you use three keywords:
virtual
marks a method in the base class as overridable. override
provides a new implementation in a derived class. base
allows access to the base class implementation.
class Logger
{
public virtual void Log(string message)
{
Console.WriteLine($"Base log: {message}");
}
}
class FileLogger : Logger
{
public override void Log(string message)
{
base.Log(message); // optional: call base method
Console.WriteLine($"File log: {message}");
}
}
The FileLogger
class overrides Log()
and
optionally calls the base version using base.Log()
. This pattern is useful when
you want to extend behavior rather than replace it.
Polymorphism through interfaces
Interfaces are a more flexible way to achieve polymorphism. Unlike classes, interfaces support multiple inheritance and don’t impose any implementation. You can use interfaces to define capabilities and treat objects based on what they can do, not what they are.
interface IPlayable
{
void Play();
}
class Song : IPlayable
{
public void Play() => Console.WriteLine("Playing song...");
}
class Podcast : IPlayable
{
public void Play() => Console.WriteLine("Playing podcast...");
}
You can now write code that works with any IPlayable
object:
void PlayMedia(IPlayable media)
{
media.Play();
}
This function doesn’t care whether it’s a song or a podcast - it just calls Play()
. That’s interface-based polymorphism.
Polymorphism and method parameters
Polymorphism shines when used in method parameters. You can write functions that accept base types or interfaces, and work with any compatible object.
void PrintArea(IShape shape)
{
Console.WriteLine($"Area: {shape.Area()}");
}
This function works with any shape - circle, square, triangle - as long as it implements IShape
. You don’t need separate functions for each type.
Polymorphism and collections
Collections are a natural place to use polymorphism. You can store different types of objects in a single list, as long as they share a common base or interface.
List shapes = new List
{
new Circle(2),
new Square(5),
new Circle(1.5)
};
double totalArea = 0;
foreach (IShape shape in shapes)
{
totalArea += shape.Area();
}
Console.WriteLine($"Total area: {totalArea}");
This loop calculates the total area of all shapes, regardless of their specific type. The code is clean, flexible, and easy to extend.
Polymorphism and testing
Polymorphism makes testing easier. You can substitute real objects with test doubles that implement the same interface. This allows you to isolate behavior and verify interactions.
interface ILogger
{
void Log(string message);
}
class MockLogger : ILogger
{
public List Messages = new List();
public void Log(string message)
{
Messages.Add(message);
}
}
You can inject MockLogger
into your code and verify that logging occurred,
without relying on console output or file I/O.
Polymorphism and design principles
Polymorphism supports several key design principles:
The Open/Closed Principle encourages you to write code that is open for extension but closed for modification. Polymorphism lets you add new types without changing existing code. The Dependency Inversion Principle promotes depending on abstractions, not concrete classes. Interfaces make this possible. The Liskov Substitution Principle ensures that objects of a derived type can be used wherever the base type is expected.
Design your APIs around interfaces and base types. This allows consumers to plug in their own implementations and keeps your code adaptable.
Common mistakes and how to avoid them
Polymorphism is powerful, but it’s easy to misuse. Here are some common pitfalls:
Forgetting to mark methods as virtual
- if a base method isn’t virtual, derived
classes can’t override it. Using concrete types in method signatures - prefer interfaces or base classes to
maximize flexibility. Overusing inheritance - favor composition and interfaces when possible. Deep inheritance
trees are hard to maintain. Not testing polymorphic behavior - always verify that your code works correctly with
different implementations.
You can combine polymorphism with generics to write highly reusable code. For example, a
method that accepts IEnumerable<T>
can work with any collection of
any type.
Summary
Polymorphism is a cornerstone of object-oriented programming. It allows you to write code that works with general types while still benefiting from specific behavior at runtime. You’ve learned how to use polymorphism with interfaces and base classes, how it supports clean design, and how to apply it in collections, parameters, and testing. By designing around abstractions and embracing polymorphism, you’ll build systems that are flexible, extensible, and robust.
In the next article, we’ll explore Casting and Type Checking - how to safely convert between types and verify object compatibility at runtime.