Polymorphism: virtual, override and abstract
Let each derived class supply its own behavior, and treat them all through their base type.
12 min read
You will learn
- Override virtual methods, including ToString
- Declare abstract classes and methods
- Call the base implementation with base.Method()
- Write code that works with any derived type
One call, many behaviors
Polymorphism ("many forms") means code written against a base type automatically uses the right behavior for each derived type. It's the main reason inheritance exists.
Animal[] zoo = { new Dog(), new Cat(), new Cow() };
foreach (Animal a in zoo)
{
Console.WriteLine(a.Speak()); // Woof! / Meow! / Moo!
}The loop knows nothing about dogs or cats — it just calls Speak(), and each object answers in its own way. Adding a Duck later requires no changes to this loop.
virtual and override
A base class marks a method virtual to say "derived classes may replace this". A derived class replaces it with override:
class Animal
{
public virtual string Speak() => "...";
}
class Dog : Animal
{
public override string Speak() => "Woof!";
}
class Cat : Animal
{
public override string Speak() => "Meow!";
}The decision about which method runs happens at run time, based on the object's real type — not the variable's type:
Animal pet = new Dog();
Console.WriteLine(pet.Speak()); // Woof! — even though the variable is an AnimalCalling the base version
An override can extend the base behavior instead of replacing it entirely:
class LoudDog : Dog
{
public override string Speak() => base.Speak().ToUpper() + "!!!"; // "WOOF!!!!"
}Overriding ToString
Every type inherits ToString() from object. By default it returns the type name, which isn't very useful. Override it:
class Product
{
public string Name { get; init; } = "";
public decimal Price { get; init; }
public override string ToString() => $"{Name} (${Price:F2})";
}
Console.WriteLine(new Product { Name = "Coffee", Price = 3.5m }); // Coffee ($3.50)Console.WriteLine, string interpolation and debuggers all use ToString().
Abstract classes
Sometimes the base class is too general to have a sensible implementation. What is the area of "a shape"? Mark the class and the method abstract:
abstract class Shape
{
public abstract double Area(); // no body: derived classes MUST implement it
public string Describe() => $"{GetType().Name}: {Area():F2}"; // regular method using it
}
class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}- You can't create an abstract class directly:
new Shape()is a compile error. - Every non-abstract derived class must override all abstract members — the compiler checks.
- Abstract classes can still have fields, constructors and normal methods.
virtual | abstract | |
|---|---|---|
| Has a body in the base class? | Yes | No |
| Must derived classes override? | Optional | Required |
| Allowed in a non-abstract class? | Yes | No |
Polymorphism replaces type checks
If you find yourself writing if (shape is Circle) ... else if (shape is Rectangle) ..., that's usually a sign the logic belongs in an overridden method on each class.
Try it
Add a Triangle class and include it in the array — the loop doesn't need to change:
C# runtime not loaded
The classes above use primary constructors (class Circle(double radius)), a C# 12 shortcut: the parameters are available throughout the class body.
Key takeaways
virtualmethods can be overridden;overridesupplies the derived behavior.- The method that runs depends on the object's actual type at run time.
abstractclasses can't be instantiated; theirabstractmembers must be overridden.- Override
ToString()to give objects a readable representation.
Exercise
Shape is abstract and every shape must implement Area. Implement Circle (π r²), Rectangle (width × height) and Square (a Rectangle whose sides are equal — reuse Rectangle's constructor). Then make TotalArea add up the areas of any mix of shapes.
C# runtime not loaded
Hint 1
Math.PI holds π. Circle's Area is Math.PI * Radius * Radius.
Hint 2
Square only needs a constructor: public Square(double side) : base(side, side) { } — Area is inherited from Rectangle.
Hint 3
TotalArea loops over the array and calls s.Area() — each shape runs its own override.
Show solution
var shapes = new Shape[] { new Circle(1), new Rectangle(2, 3), new Square(2) };
foreach (var s in shapes) Console.WriteLine(s.Describe());
Console.WriteLine($"Total: {TotalArea(shapes):F2}");
double TotalArea(Shape[] shapes)
{
double total = 0;
foreach (var shape in shapes) total += shape.Area();
return total;
}
abstract class Shape
{
public abstract double Area();
public virtual string Describe() => $"{GetType().Name} with area {Area():F2}";
}
class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public override double Area() => Math.PI * Radius * Radius;
}
class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height) { Width = width; Height = height; }
public override double Area() => Width * Height;
}
class Square : Rectangle
{
public Square(double side) : base(side, side) { }
}Quiz
4 questions · 3 correct to pass1.Given `Shape s = new Circle(1);` and Circle overrides the virtual Area, which Area runs for `s.Area()`?
2.Can you write `new Shape()` when Shape is abstract?
3.What must a non-abstract class do with an inherited abstract method?
4.Why override ToString()?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)