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:
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."; }
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:
// 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:
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; }
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:
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}"); } }
[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:
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"; }
21.5 °C -3.0 °C
Rules for overriding
- Only
virtual,abstractoroverridemembers 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.
class Base { public void Greet() { } } class Derived : Base { public override void Greet() { } }
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:
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"); }
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:
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 { }
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.
Ana $4,500.00 Ben $3,520.00 Cy $2,250.00 Total $10,270.00