Polymorphism
A variable of the base type can hold any derived object, and the override belonging to the real object is the one that runs.
Base item = new Derived(); item.VirtualMethod();Polymorphism means one variable of a base type can hold an object of any class derived from it, and a call through that variable runs the version belonging to the object actually there. The word means "many shapes". It is what virtual and override were for.
It exists to remove branching. Without it, code that handles three kinds of greeting needs a test for each kind, and a fourth kind means editing that code. With it, the caller holds a Greeter and calls Greet, and a fourth kind is a new class the caller never sees.
How it works
Two types matter for any variable. The declared type is what you wrote in the source. The runtime type is the class of the object stored there. The compiler uses the declared type to check that the method exists. At runtime, for a virtual member, the runtime type decides which body runs. That second step is called virtual dispatch.
You can therefore write a method, an array or a list in terms of the base type and pass anything derived from it.
What trips people up
Only virtual, abstract and override members work this way. A non-virtual method and a field are both chosen by the declared type, so hiding one with new gives you an object that answers differently depending on which variable you ask through. The compiler also only lets you call members that exist on the declared type, so reaching a derived-only member needs a type test first.
Example
Greeter[] greeters = [new Greeter(), new FormalGreeter(), new BriefGreeter()];
foreach (Greeter greeter in greeters)
{
// Announce knows only Greeter; the object decides which Greet runs
Console.WriteLine(Announce(greeter, "Ana"));
}
static string Announce(Greeter greeter, string name) => greeter.Greet(name);
class Greeter
{
public virtual string Greet(string name) => $"Hello, {name}.";
}
class FormalGreeter : Greeter
{
public override string Greet(string name) => $"Good evening, {name}.";
}
class BriefGreeter : Greeter
{
public override string Greet(string name) => $"Hi {name}";
}
// Output:
// Hello, Ana.
// Good evening, Ana.
// Hi AnaWhen to use it
- Loop over a list of notification channels and send each one without knowing which is email and which is SMS.
- Accept any Stream in a method so the caller can pass a file, a network socket or a memory buffer.
- Add a new report format as a new class, with no change to the code that renders reports.
More examples
An abstract base makes the dispatch mandatory
With an abstract base there is no default body to fall back to, so every charge type must answer FeeInPence and the loop can never hit a missing case. This is the form to reach for when you are replacing a switch over a type code: each old branch becomes one class.
List<Charge> charges = [new CardCharge(1200), new BankCharge(1200)];
foreach (Charge charge in charges)
{
Console.WriteLine(charge.FeeInPence());
}
abstract class Charge(int amountInPence)
{
protected int Amount => amountInPence;
public abstract int FeeInPence();
}
sealed class CardCharge(int amountInPence) : Charge(amountInPence)
{
public override int FeeInPence() => Amount * 2 / 100 + 20;
}
sealed class BankCharge(int amountInPence) : Charge(amountInPence)
{
public override int FeeInPence() => 50;
}
// Output:
// 44
// 50Fields and non-virtual methods are not polymorphic
The second line is the surprise: a Dog that says woof but calls itself an animal. Fields cannot be virtual, so a hidden field is resolved from the declared type. Expose state through a virtual property when a derived class needs to change it.
Animal[] animals = [new Animal(), new Dog()];
foreach (Animal animal in animals)
{
// Speak is virtual, so the object decides.
// Name is a field, so the declared type decides.
Console.WriteLine($"{animal.Name} says {animal.Speak()}");
}
class Animal
{
// public fields are poor style; they are here to show the behaviour
public string Name = "animal";
public virtual string Speak() => "nothing";
}
class Dog : Animal
{
// hides the field instead of overriding it, because fields cannot be virtual
public new string Name = "dog";
public override string Speak() => "woof";
}
// Output:
// animal says nothing
// animal says woofConsole.WriteLine relies on this too
Polymorphism is not something you opt into; the base class library already uses it. Console.WriteLine was compiled years before your class existed and still calls your ToString, because ToString is virtual on object. The same mechanism makes string interpolation print your type sensibly.
object[] items = [new Money(5), new Money(12)];
foreach (object item in items)
{
// WriteLine takes object and calls ToString on it.
// The Money override runs even though the parameter type is object.
Console.WriteLine(item);
}
sealed class Money(int pounds)
{
public override string ToString() => $"GBP {pounds}";
}
// Output:
// GBP 5
// GBP 12
Discussion