100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Polymorphism Explained

See how C# achieves polymorphism through virtual method dispatch, interface implementations, and method/operator overloading, letting one code path handle many shapes of data.

OOP BasicsIntermediate9 min readJul 9, 2026
Analogies

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.

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

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

Runtime 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

Was this page helpful?

Topics covered

#CStudyNotes#Programming#PolymorphismExplained#Polymorphism#Explained#Runtime#Virtual#OOP#StudyNotes#SkillVeris