Polymorphism

Polymorphism ("many forms") means one piece of code can work with objects of different types, and each object responds in its own way. You write shape.Area() once, and circles, squares and triangles each compute their own area. In C#, this is done with virtual methods that derived classes override.

virtual and override

A base class marks a method virtual to say "derived classes may replace this". A derived class replaces it with override. When the method is called, the runtime looks at the object's actual type, not the variable's type:

C#
Animal[] animals = [new Dog("Rex"), new Cat("Tom"), new Animal("Generic")];

foreach (Animal a in animals)
{
    Console.WriteLine($"{a.Name}: {a.Speak()}");
}

class Animal(string name)
{
    public string Name { get; } = name;
    public virtual string Speak() => "...";
}

class Dog(string name) : Animal(name)
{
    public override string Speak() => "Woof!";
}

class Cat(string name) : Animal(name)
{
    public override string Speak() => "Meow.";
}
Output
Rex: Woof!
Tom: Meow.
Generic: ...

The loop doesn't know or care which kind of animal it has. Add a Cow class tomorrow and the loop works with it unchanged — that's the real power of polymorphism.

Replacing type checks

Without polymorphism you'd write code that inspects the type and branches. It works, but every new type means editing this code — and it's easy to forget one place:

C#
// Fragile: must be updated for every new shape
double Area(object shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Square s => s.Side * s.Side,
    _ => throw new NotSupportedException(),
};

With polymorphism, each class knows its own area:

C#
List<Shape> shapes = [new Circle(1.5), new Square(2), new Triangle(3, 4)];

double total = 0;
foreach (Shape s in shapes)
{
    Console.WriteLine($"{s.Name,-9}{s.Area(),8:F2}");
    total += s.Area();
}
Console.WriteLine($"{"Total",-9}{total,8:F2}");

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 Square(double side) : Shape
{
    public override string Name => "Square";
    public override double Area() => side * side;
}

class Triangle(double b, double h) : Shape
{
    public override string Name => "Triangle";
    public override double Area() => 0.5 * b * h;
}
Output
Circle       7.07
Square       4.00
Triangle     6.00
Total       17.07

Properties can be virtual too, as Name shows.

Extending base behaviour

An override can call base.Method() to keep the original behaviour and add to it:

C#
Notifier n = new SmsNotifier();
n.Send("Your order has shipped");

class Notifier
{
    public virtual void Send(string message)
    {
        Console.WriteLine($"[log] sending: {message}");
    }
}

class SmsNotifier : Notifier
{
    public override void Send(string message)
    {
        base.Send(message);
        string sms = message.Length > 20 ? message[..17] + "..." : message;
        Console.WriteLine($"[sms] {sms}");
    }
}
Output
[log] sending: Your order has shipped
[sms] Your order has sh...

Overriding object's methods

ToString, Equals and GetHashCode are virtual methods on object. Overriding ToString is the most common example of polymorphism you'll write:

C#
object[] items = [new Temperature(21.5), new Temperature(-3)];
foreach (object o in items)
{
    Console.WriteLine(o);    // calls the override, even through an object variable
}

class Temperature(double celsius)
{
    public override string ToString() => $"{celsius:F1} °C";
}
Output
21.5 °C
-3.0 °C

Rules for overriding

  • Only virtual, abstract or override members can be overridden. Methods are not virtual by default in C# (unlike Java).
  • The override must have the same signature and the same access level.
  • An override is itself virtual, so a further subclass can override it again — unless it's marked sealed override.
C#
class Base
{
    public void Greet() { }
}

class Derived : Base
{
    public override void Greet() { }
}
Build error
Program.cs(8,26): error CS0506: 'Derived.Greet()': cannot override inherited member 'Base.Greet()' because it is not marked virtual, abstract, or override

override versus new

Lesson 36 showed that redeclaring a non-virtual method hides it. Writing new silences the warning but doesn't make it polymorphic. Compare the two side by side:

C#
Base a = new WithOverride();
Base b = new WithNew();
a.Who();
b.Who();

class Base
{
    public virtual void Who() => Console.WriteLine("Base");
}

class WithOverride : Base
{
    public override void Who() => Console.WriteLine("WithOverride");
}

class WithNew : Base
{
    public new void Who() => Console.WriteLine("WithNew");
}
Output
WithOverride
Base

With override, the object's real type decides. With new, the variable's type decides, so calling through a Base variable runs the base version. You'll almost always want override.

Type checks and casts

Sometimes you do need the specific type. Use is with a pattern, or as, which returns null instead of throwing if the cast fails. A direct cast (Dog)a throws an InvalidCastException when it's wrong:

C#
Animal[] pets = [new Dog(), new Cat()];

foreach (Animal pet in pets)
{
    if (pet is Dog dog)
        dog.Fetch();

    Cat? cat = pet as Cat;
    Console.WriteLine(cat is null ? "not a cat" : "a cat");
}

Dog wrong = (Dog)pets[1];

class Animal { }
class Dog : Animal { public void Fetch() => Console.WriteLine("Fetching!"); }
class Cat : Animal { }
Output
Fetching!
not a cat
a cat
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'Cat' to type 'Dog'.

If you find yourself checking types a lot, it's often a sign the behaviour belongs in a virtual method instead.

Try it yourself

Each employee type calculates pay differently. Add a Contractor paid per hour.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Ana   $4,500.00
Ben   $3,520.00
Cy    $2,250.00
Total $10,270.00