AlgoMaster Logo

Polymorphism Basics

Last Updated: May 17, 2026

11 min read

Polymorphism is the third pillar of object-oriented programming, sitting next to inheritance and encapsulation. The word comes from Greek roots meaning "many forms," and in C# that translates to one method name, one operator, or one base reference being able to take on different concrete behaviors depending on context. This chapter introduces the idea at a conceptual level and shows why it matters in real e-commerce code.

What Polymorphism Actually Means

The textbook one-liner is "one interface, many forms." A more practical phrasing: the same call site can produce different runtime behavior depending on what it is called on, or what arguments it is called with. The "interface" in this definition is the call you write (item.Process(), cart + product, Send(message)), and the "many forms" are the different method bodies that can actually run.

The key insight is that polymorphism isn't a single C# feature. It's a property of several language features working together. When you write cart.AddItem(product), the compiler and runtime decide which version of AddItem to invoke based on rules that depend on which kind of polymorphism is in play. There are two broad categories, and each has its own mechanism:

  1. Compile-time polymorphism picks the method at compile time, based on information the compiler can see in the source code.
  2. Runtime polymorphism picks the method at runtime, based on the actual type of the object behind a reference.

Both are useful. Both show up constantly in real C# code. And they are easy to confuse because the surface syntax looks similar.

A small contrast makes the difference concrete:

The two calls look almost identical, but the compiler picks different methods based on the number and types of arguments. That decision happens before the program ever runs. Now compare runtime polymorphism:

Every loop iteration calls item.Send(). The reference type is always Notification. But the runtime walks down to the actual object on the heap and picks the most-derived Send for each one. That decision happens at runtime, not at compile time.

These two examples cover the punchline of the entire chapter. Everything else in this section unpacks how each mechanism works in detail.

Why Polymorphism Matters

Polymorphism earns its place in OOP because it lets code handle many concrete types through a single shared abstraction. Without it, you end up with long chains of type checks that grow every time someone adds a new variant. With it, the dispatch is the language's job, and new variants slot in without touching the calling code.

Imagine an order-processing service that has to handle different kinds of payment. A version without polymorphism might look like this:

The code works, but every new payment type means another branch in the if-else chain. Worse, the same chain probably shows up in other places (refund logic, reporting, fraud checks), and they will drift apart over time. The shape of this code is brittle.

A polymorphic version replaces the type-tag check with a virtual method call:

The OrderProcessor.Process method no longer cares what kind of payment it received. It hands the work to the payment itself and lets dispatch sort it out. Adding a LoyaltyPointsPayment later means writing one new class. The processor stays untouched.

This points at a design principle that polymorphism enables: the Open-Closed Principle. A class should be open for extension (you can add new behavior by adding new types) but closed for modification (existing code doesn't have to change to accept those new types). The if-else version violates this principle. The polymorphic version honors it.

Polymorphism also pays off in collections of mixed types. A Cart can hold any subclass of CartItem and treat them uniformly:

One loop, three completely different total-calculation rules. The loop doesn't know or care which kind of item it has at any moment, and adding a fourth kind doesn't change the loop at all. This is the everyday payoff of runtime polymorphism in production C# code.

The Two Flavors of Polymorphism

C# offers polymorphism through two distinct mechanisms. Each chapter in this section explores one of them in detail. A short tour helps orient you:

The diagram splits polymorphism into the two categories and lists the C# features that fall under each. Compile-time polymorphism in C# comes from overloading: writing multiple methods with the same name but different parameter lists, or defining operators (+, -, ==) for your own types. The compiler picks the right one at compile time based on the argument types.

Runtime polymorphism comes from inheritance plus virtual dispatch. A base class declares a method as virtual (or abstract), derived classes override it, and at runtime the call goes to the derived version regardless of which reference type was used to make the call. Covariant return types are a small refinement that lets an overriding method return a more specific type than the base.

Here is how each lesson in this section maps to the diagram:

  • _Compile-Time Polymorphism_. Method overloading rules, operator overloading, why ambiguous overloads fail to compile, and what the compiler actually does to pick a candidate.
  • _Runtime Polymorphism_. How virtual and override cooperate, how the CLR finds the right method at runtime, and the everyday patterns that rely on this dispatch.
  • _virtual, override & new_. A direct comparison of the three keywords, how new (method hiding) breaks polymorphism in a way that surprises people, and which one to reach for in different situations.
  • _Covariant Return Types_. A focused look at how, since C# 9, an overriding method can narrow its return type. Useful for factory methods and cloning patterns.

You will not need any of those details to understand the rest of this lesson. The point of the tour is that every flavor of polymorphism in C# fits somewhere in that tree.

Static Dispatch vs Dynamic Dispatch

The mechanism behind the two categories has a name. Compile-time polymorphism uses static dispatch: the compiler resolves which method body to call by looking at the source, baking the decision into the compiled IL. The IL instruction is call, and it names the exact method to invoke.

Runtime polymorphism uses dynamic dispatch: the IL records that the call must look up the right method based on the object's actual type at runtime. The IL instruction is callvirt, and the runtime consults a hidden table on the object (called a method table or vtable) to find the right slot.

Here is a small mental model:

The diagram shows the same call site (payment.Process()) routed through one of two paths depending on whether the method is virtual. If it is, the call becomes dynamic and the runtime does the picking. If it isn't, the call is static and the compiler does the picking. Method overloading falls into the static-dispatch path: the compiler picks among the overloads based on the argument types it sees, then emits the chosen method directly.

You don't need to know the IL instructions to use polymorphism. The mental shortcut that matters is: virtual methods are decided when the program runs; everything else is decided when it compiles. That single rule predicts the behavior of every polymorphism feature in C#.

The cost callout is here for completeness. Don't optimize away virtual dispatch on instinct. Virtual calls are how most idiomatic C# polymorphism works, and the JIT is good at inlining virtual calls in the common case where the type can be predicted.

Polymorphism in Action: A Notification Outbox

The earlier notification example showed runtime polymorphism in the smallest possible form. Stretching it a little further makes the design payoff clearer.

Imagine a notification outbox that processes messages from a single base type. The outbox doesn't know what kinds of notification exist; it only knows they all support Send.

Every line in Outbox.Flush calls notification.Send(). The variable's static type is Notification. Three different method bodies run, one per object, because Send is virtual (specifically, abstract, which is virtual without a default implementation). That's runtime polymorphism doing the dispatching.

Three things are worth noting about this design:

  1. The Outbox class never mentions EmailNotification, SmsNotification, or PushNotification. It works against the abstraction.
  2. Adding a fourth notification type, like WebhookNotification, requires no change to Outbox. Define the new class, override Send, and queue an instance.
  3. The polymorphism is hidden behind a simple call. There's no switch on the type, no reflection, no manual dispatch table. The language does the routing.

This pattern shows up in just about every well-factored C# application: dispatchers, handlers, strategies, plugins, validators. The mechanics are the same as the toy example. The base class declares the contract, the derived classes fill in the details, and the calling code works through the base reference.

Common Misconceptions

A few beliefs about polymorphism come up often enough to be worth correcting up front.

"Polymorphism is the same as inheritance." Inheritance is one way to enable polymorphism, but they aren't the same. Inheritance is a structural relationship between classes. Polymorphism is a behavior at call sites. You can have inheritance without polymorphism (a derived class that doesn't override anything), and you can have polymorphism without inheritance (method overloading is polymorphism without any inheritance). Interfaces also enable polymorphism, and an interface is not inheritance in the same sense as class Derived : Base.

"Polymorphism is just overloading." Overloading is one of two kinds of polymorphism, and the more limited one. The compiler picks an overload at compile time based on argument types. That's useful, but it doesn't give you the open-closed payoff that overriding does. Conflating the two leads people to write if (item is Card) ... else if (item is Coupon) ... instead of using virtual dispatch.

"Polymorphism is a feature." It's a property of several features working together. C# doesn't have a polymorphism keyword. It has virtual, override, abstract, new, method overloading, operator overloading, interfaces, and generics, and those together produce polymorphic behavior. Asking "does C# support polymorphism?" is a little like asking "does C# support readable code?" The answer is "yes, through many mechanisms, used well."

"Polymorphism always involves dynamic dispatch." Only runtime polymorphism does. Method overloading is resolved at compile time and never touches the method table at runtime. The compiler picks the overload and emits a direct call. It is still a form of polymorphism: same name, different behavior depending on context.

"Polymorphism makes code slower." Virtual dispatch has a small cost, but the cost is rarely the bottleneck in real applications. In most cases, the design clarity polymorphism buys you is worth far more than the cycles it spends on a method table lookup. The performance discussion is real, but it belongs in profiler output, not in design discussions about whether to use virtual.

A short reference table captures these in one place:

BeliefReality
Polymorphism = inheritanceInheritance is structural; polymorphism is behavioral. Related but not the same.
Polymorphism = overloadingOverloading is one kind; runtime polymorphism with virtual/override is the more powerful kind.
Polymorphism is a single C# featureIt's a property emerging from several features (virtual, override, abstract, overloading, interfaces, generics).
Polymorphism always uses dynamic dispatchCompile-time polymorphism uses static dispatch. The compiler picks the overload.
Polymorphism hurts performanceVirtual call cost is small. Most code is bottlenecked elsewhere, and the JIT can inline many virtual calls.

Where Each Mechanism Fits

Knowing the categories is one thing; knowing when to reach for each is the next step. The next four chapters cover the mechanics in detail. As a rough orientation:

You want to...MechanismChapter
Provide multiple ways to call the same method with different argument shapesMethod overloading2
Define how +, ==, or other operators work on your own typeOperator overloading2
Let derived classes replace a base method's behavior, with dispatch by object typevirtual + override3
Force derived classes to provide an implementation, with no default in the baseabstract method3
Understand exactly how new differs from override (and why it's usually the wrong choice)new keyword hiding4
Have an overriding method return a more specific type than its baseCovariant return types5

Each row corresponds to a different problem and a different language feature. Once you can name the problem you have, the mechanism falls out naturally. Most everyday code reaches for overloading and virtual/override. Operator overloading shows up around custom value-like types (money, vectors, time spans). Abstract methods show up when the base class is a template waiting for subclasses to complete it. new and covariant returns are edge cases.

A small worked example shows how overloading and overriding can coexist in the same class without any conflict. Both are polymorphism, but they answer different questions.

The first call uses runtime polymorphism: Send(string) is abstract on the base, the runtime dispatches to EmailNotification.Send. The second call uses compile-time polymorphism to pick Send(string, string), which is a non-virtual base method that internally calls back into the virtual Send(string) and so re-enters runtime dispatch. The two mechanisms compose cleanly.

Putting the Pieces Together

A short summary of the model that will carry through the rest of the section:

  1. Polymorphism is "one call site, many behaviors." That single idea generalizes to overloading (different argument types), overriding (different runtime types), and interface dispatch (different implementations of the same contract).
  2. C# delivers polymorphism through two paths: compile-time (overloading, operators) and runtime (virtual/override, abstract methods, interfaces).
  3. Static dispatch means the compiler picks the target. Dynamic dispatch means the runtime picks the target. Method overloading is static. Virtual methods are dynamic. Non-virtual instance methods are static. Abstract methods are dynamic.
  4. The reason to care about any of this is design: polymorphism replaces sprawling type checks with single virtual calls, which makes code easier to extend and easier to read.
  5. Inheritance, polymorphism, overloading, and the keyword new are related but distinct. They look similar in syntax and trip people up regularly. The remaining lessons in this section tease them apart.

Keep the static-vs-dynamic dispatch picture in mind as you move into the _Compile-Time Polymorphism_ lesson. Overloading is the first concrete mechanism you'll see, and that lesson spends most of its time on the compiler's rules for picking an overload when more than one candidate matches. Those rules only make sense once you've internalized that the decision happens before the program runs.

Summary

  • Polymorphism means "one call site, many behaviors." It is a property that emerges from several C# features, not a single feature.
  • C# has two kinds: compile-time polymorphism (method overloading, operator overloading) and runtime polymorphism (virtual/override, abstract methods, interfaces).
  • Compile-time polymorphism uses static dispatch: the compiler picks the target method from the source. Runtime polymorphism uses dynamic dispatch: the runtime picks the target from the object's actual type through the method table.
  • Polymorphism enables the Open-Closed Principle: adding a new variant means adding a new class, not modifying existing callers. Long if/else chains on a type-tag field are usually a sign that runtime polymorphism would be a better fit.
  • Inheritance and polymorphism are related but distinct. Inheritance is a class relationship; polymorphism is a dispatch property. Overloading is polymorphism without inheritance.
  • The mental shortcut: virtual methods are decided at runtime; everything else is decided at compile time. That single rule predicts how every dispatch case in C# will resolve.
  • Virtual calls cost a small lookup compared to direct calls. The cost is rarely the bottleneck in real applications, and the JIT can inline virtual calls in many cases.
  • The rest of this section unpacks each mechanism: _Compile-Time Polymorphism_ covers compile-time polymorphism in depth, _Runtime Polymorphism_ covers runtime polymorphism, _virtual, override & new_ contrasts the three keywords, and _Covariant Return Types_ covers covariant return types.