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.
On this page · 5 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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}";
}
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#
- The list is typed List<Shape> but holds three different kinds of shape.
- shape.Area() calls Circle.Area, Rectangle.Area or the inherited Rectangle.Area for Square — decided per object at runtime.
- Square reuses Rectangle’s Area and only overrides Name.
- {shape.Name,-10} pads the name to 10 characters so the numbers line up.
- 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#
A switch on shape.GetType() must be edited for every new shape. Put the behaviour in the class as an override.
Without override, the base version runs when the object is used through a base variable. Watch for the compiler warning CS0114.
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?
Show solution
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#
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"; }
Want more? Practise all C# MCQs · C# interview questions
My notes#
Saved in this browser.