.NET Academy

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 Animal

Calling 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.
virtualabstract
Has a body in the base class?YesNo
Must derived classes override?OptionalRequired
Allowed in a non-abstract class?YesNo

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:

Program.cs
Loading editor…

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

  • virtual methods can be overridden; override supplies the derived behavior.
  • The method that runs depends on the object's actual type at run time.
  • abstract classes can't be instantiated; their abstract members 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.

Program.cs
Loading editor…

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 pass
  1. 1.Given `Shape s = new Circle(1);` and Circle overrides the virtual Area, which Area runs for `s.Area()`?

  2. 2.Can you write `new Shape()` when Shape is abstract?

  3. 3.What must a non-abstract class do with an inherited abstract method?

  4. 4.Why override ToString()?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)