C# · Object-Oriented Programming

Polymorphism

Polymorphism means “many forms”. You write code against a general type — a Shape — and each specific object (Circle, Square) does the right thing on its own.

Intermediate 5 min Lesson 14 of 26Examples target C# 13 on .NET 9 unless stated otherwise.
Before you start:Inheritance
0/26 in C#
On this page · 5 min · Intermediate

In simple words#

Say “speak” to a dog, a cat and a parrot — each answers in its own way. One instruction, many behaviours: that is polymorphism.

Polymorphism
Same call, different behaviour per type.
virtual / override
Let a child class replace a method.
Overloading
Same method name, different parameters.

Learning objectives#

  • Treat different derived objects through one base type
  • Explain runtime polymorphism (overriding)
  • Explain compile-time polymorphism (overloading)
  • Avoid type-checking chains with polymorphism

Explanation#

Runtime polymorphism: a variable of type Shape can hold a Circle or a Rectangle. Calling shape.Area() runs the override of the actual object, decided while the program runs.

This removes if/else chains such as if (shape is Circle) ... else if (shape is Rectangle) .... Adding a new shape means adding a class, not editing old code.

Compile-time polymorphism is method overloading: several methods with the same name but different parameters. The compiler picks one from the argument types.

In the example, the classes use primary constructors (C# 12): class Circle(double r) makes r available throughout the class body.

Syntax#

C# Illustrative
Shape s = new Circle(2);
s.Area();            // runs Circle.Area — chosen at runtime


string Format(int n) => ...;
string Format(double d) => ...;   // overload — chosen at compile time

Code example: Shapes and overloads#

C#Program.cs Illustrative
List<Shape> shapes = [new Circle(1), new Rectangle(3, 4), new Square(2)];


foreach (var shape in shapes)
{
    Console.WriteLine(
quot;
{shape.Name,-10}{shape.Area():F2}");
} Console.WriteLine(
quot;Total area:
{shapes.Sum(s => s.Area()):F2}");
Console.WriteLine(Printer.Format(5)); Console.WriteLine(Printer.Format(5.5)); Console.WriteLine(Printer.Format("five")); class Shape { public virtual string Name => "Shape"; public virtual double Area() => 0; } class Circle(double radius) : Shape { public override string Name => "Circle"; public override double Area() => Math.PI * radius * radius; } class Rectangle(double width, double height) : Shape { public override string Name => "Rectangle"; public override double Area() => width * height; } class Square(double side) : Rectangle(side, side) { public override string Name => "Square"; } static class Printer { public static string Format(int n) =>
quot;int
{n}";
public static string Format(double d) =>
quot;double
{d}";
public static string Format(string s) =>
quot;string
{s}";
}
Expected output
Circle    3.14
Rectangle 12.00
Square    4.00
Total area: 19.14
int 5
double 5.5
string five

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The list is typed List<Shape> but holds three different kinds of shape.
  2. shape.Area() calls Circle.Area, Rectangle.Area or the inherited Rectangle.Area for Square — decided per object at runtime.
  3. Square reuses Rectangle’s Area and only overrides Name.
  4. {shape.Name,-10} pads the name to 10 characters so the numbers line up.
  5. Printer.Format has three overloads; the compiler picks one from the argument’s type.

Real-world use case#

A payment service holds a list of IPaymentProvider objects (Razorpay, Stripe, PayPal). Checkout calls provider.Charge(amount) without knowing which provider it is talking to.

Common mistakes#

Type-checking instead of overriding

A switch on shape.GetType() must be edited for every new shape. Put the behaviour in the class as an override.

Hiding with new by accident

Without override, the base version runs when the object is used through a base variable. Watch for the compiler warning CS0114.

Overloads that differ only by return type

int Parse(string s) and double Parse(string s) cannot both exist. Overloads must differ in parameters.

Best practices#

  • Program against the base type or interface
  • Put type-specific behaviour in overrides, not in if/else chains
  • Keep overloads consistent: same meaning, different inputs
  • Use abstract members when there is no sensible default

Remember this#

Same call, different behaviour, depending on the real object.

Overriding = runtime. Overloading = compile time.

New type? Add a class, do not edit a switch.

Practice exercise#

Your task: Create a base class Notification with a virtual Send() returning "Sending notification", and EmailNotification / SmsNotification that override it. Loop over a list of both and print the results.

Need a hint?
List<Notification> list = [new EmailNotification(), new SmsNotification()];
Show solution
C# Illustrative
List<Notification> list = [new EmailNotification(), new SmsNotification()];
foreach (var n in list) Console.WriteLine(n.Send());
// Sending email
// Sending SMS


class Notification { public virtual string Send() => "Sending notification"; }
class EmailNotification : Notification { public override string Send() => "Sending email"; }
class SmsNotification : Notification { public override string Send() => "Sending SMS"; }

Mini quiz#

Polymorphism quizPredict the output Question 1 of 3

What is printed?

Animal a = new Dog();
Console.WriteLine(a.Speak());


class Animal { public virtual string Speak() => "..."; }
class Dog : Animal { public override string Speak() => "Woof"; }
Score so far: 0/0

Want more? Practise all C# MCQs · C# interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.