Polymorphism Explained
Polymorphism — literally 'many forms' — is the ability to treat objects of different concrete types through a single, common interface, letting each type respond in its own way to the same call. C# supports two broad categories: runtime (dynamic) polymorphism, achieved through virtual methods, abstract classes, and interfaces, where the exact code that runs is decided by the object's actual type at execution time; and compile-time (static) polymorphism, achieved through method overloading and operator overloading, where the compiler picks the right member based on argument types when it compiles the call. Runtime polymorphism is what lets a single 'foreach' loop over a List<Shape> correctly compute the area of circles, squares, and triangles alike without an if/else chain checking types.
Cricket analogy: A commentator watching a match doesn't need an if/else chain to know how each player scores — a Kohli cover-drive and a Bumrah yorker each respond to 'perform' in their own way, decided at the moment of play, just like runtime polymorphism over a List<Shape>.
Runtime polymorphism via virtual dispatch
When a method is declared 'virtual' in a base class (or 'abstract', or comes from an interface) and 'override'-ridden in derived classes, calling it through a base-typed or interface-typed reference still executes the most-derived implementation. The CLR determines this at runtime by consulting the object's actual type, via a mechanism conceptually similar to a virtual method table. This is the mechanism that lets polymorphic collections, dependency injection, and plugin architectures work — code written against an abstraction ('Shape', 'ILogger') automatically picks up new behaviors when new derived types are introduced, with zero changes to the calling code.
Cricket analogy: When a captain calls 'bat' through the generic 'Player' role, the CLR-like team sheet consults the actual named player — Root overrides with a cover drive, Bumrah overrides with a defensive block — and a new signing added mid-season needs zero changes to the captain's call.
public abstract class Shape
{
public abstract double Area();
}
public class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}
public class Square : Shape
{
public double Side { get; init; }
public override double Area() => Side * Side;
}
List<Shape> shapes = [new Circle { Radius = 2 }, new Square { Side = 3 }];
double total = shapes.Sum(s => s.Area()); // dispatches to each shape's own Area()Polymorphism through interfaces
Interfaces provide polymorphism without requiring a shared base class, which is invaluable because C# only allows single class inheritance. Any number of unrelated types can implement 'IComparable<T>', 'IDisposable', or a custom 'IPaymentProcessor', and code that depends only on the interface works uniformly across all of them. This is often preferred over base-class polymorphism for cross-cutting capabilities, since it avoids forcing unrelated types into an artificial hierarchy just to share a method signature.
Cricket analogy: A wicketkeeper and a groundskeeper share no common ancestor role, yet both can implement 'ICertified' for safety compliance — interfaces let unrelated cricket roles share a capability without forcing them into an artificial shared hierarchy, since a player can only belong to one team lineage.
Compile-time polymorphism: overloading
Method overloading lets a class define multiple methods with the same name but different parameter lists; the compiler resolves which one to call based on the static types of the arguments at the call site. Operator overloading extends this idea to operators like '+' or '==', letting user-defined types participate in natural syntax (e.g., adding two Money values, or comparing two Vector2 instances). Unlike virtual dispatch, overload resolution happens entirely at compile time — there is no runtime lookup based on the object's actual type.
Cricket analogy: A scorer's 'RecordShot' method has different versions for a four and a six, resolved by which values are passed in, decided the instant the ball crosses the boundary — no runtime lookup needed, just like C#'s compile-time overload resolution.
public readonly struct Money
{
public decimal Amount { get; }
public Money(decimal amount) => Amount = amount;
public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
public override string ToString() => $"${Amount:F2}";
}
var total = new Money(10.50m) + new Money(4.25m);
Console.WriteLine(total); // $14.75Runtime polymorphism (virtual/override/interfaces) is sometimes called 'subtype polymorphism' and is the mechanism behind design patterns like Strategy and Template Method. Overloading is 'ad-hoc polymorphism' and is closer to what C++ templates or Haskell type classes achieve in different ways.
A field or property of a base type accessed via that base type only exposes base-type members even if the runtime object is a derived type — polymorphism applies to virtual method calls, not to which members are visible without a cast. Accessing a Circle-only property through a 'Shape' reference requires a pattern match or cast first.
- Polymorphism lets code written against a common type work correctly with many concrete types.
- Runtime (subtype) polymorphism uses virtual/override or interface implementations, resolved by the object's actual type at execution time.
- Compile-time (ad-hoc) polymorphism uses method and operator overloading, resolved by argument types when compiled.
- Interfaces enable polymorphism across unrelated types, sidestepping single-inheritance limits.
- Virtual dispatch is what powers dependency injection, plugin systems, and polymorphic collections.
- Overload resolution never consults the runtime type — it is fully determined at compile time.
Practice what you learned
1. Which mechanism resolves method overload selection?
2. Why are interfaces especially useful for achieving polymorphism in C#?
3. What determines which override executes when calling a virtual method through a base-typed reference?
4. What is the term for polymorphism achieved via method/operator overloading?
5. If 'Shape shape = new Circle { Radius = 3 };', what happens if you try 'shape.Radius' directly?
Was this page helpful?
You May Also Like
Inheritance in C#
Learn how C# classes derive from a single base class to reuse and extend behavior, including virtual/override methods, base member access, and sealing.
Interfaces in C#
Understand interfaces as pure behavioral contracts that any type can implement, including default interface methods, explicit implementation, and multiple interface inheritance.
Abstract Classes
Learn how abstract classes provide a partially-implemented base type that cannot be instantiated directly, mixing shared implementation with members derived classes must supply.
Generics Explained
Understand how C# generics let you write type-safe, reusable code that works across many types without sacrificing performance or requiring casts.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics