C# Inheritance and Polymorphism

Share code between related classes with inheritance, call base constructors, override virtual methods to change behaviour, treat different objects through one base type with polymorphism, and use abstract and sealed classes.

Lesson 13 of 43Intermediate18 min read

Many things in the real world are specialised versions of something more general. A savings account and a current account are both bank accounts. A car and a bicycle are both vehicles. Inheritance lets one class build on another: the derived class (or subclass) automatically gets the fields, properties and methods of its base class, and adds or changes what it needs.

Inheritance avoids repeating shared code, but its real power is polymorphism, which means "many forms". You can store different kinds of objects in one list of the base type, call the same method on each, and every object responds in its own way. A payroll system calls CalculatePay() on every employee, and salaried staff, hourly staff and contractors each work it out differently.

In this lesson you'll create class hierarchies, call base constructors, override methods with virtual and override, write abstract classes that define a template for subclasses, check types with is, and learn when inheritance is the wrong tool.

Base and derived classes

C#
class Animal
{
    public string Name { get; }
    public Animal(string name) => Name = name;
    public void Eat() => Console.WriteLine($"{Name} is eating");
}

class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public void Fetch() => Console.WriteLine($"{Name} fetches the ball");
}
  • class Dog : Animal means Dog inherits from Animal. A Dog is an Animal.
  • : base(name) calls the base class constructor so it can set up its part of the object.
  • Dog has Name and Eat() from Animal, plus its own Fetch().
  • A class can inherit from only one base class. Every class ultimately inherits from object, which is where ToString, Equals and GetHashCode come from.
  • protected members are visible in derived classes but not to outside code.

virtual, override and polymorphism

A base class marks a method virtual to allow derived classes to replace it with override:

C#
class Shape
{
    public virtual double Area() => 0;
}
class Circle : Shape
{
    public double Radius { get; init; }
    public override double Area() => Math.PI * Radius * Radius;
}

Shape s = new Circle { Radius = 2 };
Console.WriteLine(s.Area());     // 12.566..., the Circle version runs

Even though s is declared as Shape, the object's real type decides which version runs. That's polymorphism. Inside an override, base.Area() calls the base version, which is handy for extending behaviour rather than replacing it.

abstract and sealed

An abstract class can't be created with new; it exists only to be inherited. It can contain abstract members with no body, which every non-abstract subclass must override:

C#
abstract class Employee
{
    public string Name { get; init; } = "";
    public abstract decimal MonthlyPay();
}

A sealed class can't be inherited from at all, and sealed override stops further overriding. string is a sealed class.

To check an object's type, use is with a pattern: if (shape is Circle c) Console.WriteLine(c.Radius);. The as operator converts or gives null.

Hiding a method with new instead of overriding it

If a derived class declares a method with the same name as a non-virtual method in its base class, it doesn't override it. It hides it, and the compiler warns you with CS0108. Adding the new keyword silences the warning, but the behaviour is very different from override:

C#
class Animal
{
    public void Speak() => Console.WriteLine("Animal sound");
    public virtual void Describe() => Console.WriteLine("An animal");
}
class Dog : Animal
{
    public new void Speak() => Console.WriteLine("Woof");
    public override void Describe() => Console.WriteLine("A dog");
}

Animal a = new Dog();
a.Speak();      // Animal sound: the variable's type decides
a.Describe();   // A dog: the object's type decides

With hiding, the declared type of the variable chooses which method runs, so the same object behaves differently depending on how you refer to it. That's almost never what you want. When you see CS0108, the fix is usually to make the base method virtual and use override, not to add new.

C# Object-Oriented Programming Syntax

Inheritance syntax

class Base
{
    protected int x;
    public Base(int x) => this.x = x;
    public virtual string Describe() => "base";
}
class Derived : Base
{
    public Derived() : base(5) { }
    public override string Describe() => base.Describe() + " + derived";
}
abstract class Shape { public abstract double Area(); }
sealed class Square : Shape { public override double Area() => 1; }
if (obj is Derived d) { }

C# Object-Oriented Programming Summary

  • class B : A makes B inherit A's members; C# allows one base class.
  • : base(...) calls the base constructor; protected members are visible to subclasses.
  • virtual methods can be replaced with override; the object's real type decides which runs.
  • abstract classes can't be instantiated and can force subclasses to implement members.
  • sealed prevents inheritance; is and as check and convert types.

C# Object-Oriented Programming Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A payroll with polymorphism

C#
var staff = new List<Employee>
{
    new SalariedEmployee("Ada", 42000m),
    new HourlyEmployee("Ben", 15.50m, 120),
    new HourlyEmployee("Cy", 18m, 90)
};

decimal total = 0;
foreach (Employee e in staff)
{
    decimal pay = e.MonthlyPay();
    total += pay;
    Console.WriteLine($"{e.Name,-4} {e.Kind,-9} {pay,9:F2}");
}
Console.WriteLine($"Total payroll: {total:F2}");

abstract class Employee(string name)
{
    public string Name { get; } = name;
    public virtual string Kind => "Staff";
    public abstract decimal MonthlyPay();
}

class SalariedEmployee(string name, decimal salary) : Employee(name)
{
    public override string Kind => "Salaried";
    public override decimal MonthlyPay() => Math.Round(salary / 12, 2);
}

class HourlyEmployee(string name, decimal rate, int hours) : Employee(name)
{
    public override string Kind => "Hourly";
    public override decimal MonthlyPay() => rate * hours;
}
Output
Ada  Salaried    3500.00
Ben  Hourly      1860.00
Cy   Hourly      1620.00
Total payroll: 6980.00
Line-by-line explanation
  1. Line 1One list holds different kinds of employee through their shared base type.
  2. Line 11Each object runs its own MonthlyPay; the loop doesn't need to know which kind it has.
  3. Line 17An abstract class can't be created directly.
  4. Line 21Every non-abstract subclass must provide MonthlyPay.
  5. Line 24Primary constructor parameters pass name on to the base class.
  6. Line 2742000 divided by 12 is 3500.
  7. Line 3315.50 times 120 hours is 1860.
Example 2

Extending behaviour with base

C#
Notification[] items =
{
    new Notification("Your order shipped"),
    new UrgentNotification("Payment failed")
};

foreach (var n in items)
{
    Console.WriteLine(n.Format());
}

class Notification(string message)
{
    public string Message { get; } = message;
    public virtual string Format() => $"[info] {Message}";
}

class UrgentNotification(string message) : Notification(message)
{
    public override string Format() => base.Format().Replace("info", "URGENT") + " (action needed)";
}
Output
[info] Your order shipped
[URGENT] Payment failed (action needed)
Line-by-line explanation
  1. Line 9The same call produces different results for each object.
  2. Line 15virtual allows subclasses to override Format.
  3. Line 20base.Format() reuses the base version and then extends it.
Example 3

Type checks with is

C#
Vehicle[] fleet = { new Car(4), new Bike(), new Car(2) };

int seats = 0;
foreach (Vehicle v in fleet)
{
    if (v is Car car)
    {
        seats += car.Seats;
    }
    Console.WriteLine($"{v.GetType().Name}: {v.Wheels} wheels");
}
Console.WriteLine($"Total car seats: {seats}");

abstract class Vehicle
{
    public abstract int Wheels { get; }
}
class Car(int seats) : Vehicle
{
    public int Seats { get; } = seats;
    public override int Wheels => 4;
}
class Bike : Vehicle
{
    public override int Wheels => 2;
}
Output
Car: 4 wheels
Bike: 2 wheels
Car: 4 wheels
Total car seats: 6
Line-by-line explanation
  1. Line 6is checks the real type and, if it matches, gives a typed variable to use.
  2. Line 8Seats only exists on Car, so it needs the converted variable.
  3. Line 10GetType().Name shows the object's actual class.
  4. Line 16Abstract properties work just like abstract methods.

Common C# Object-Oriented Programming Mistakes

Forgetting virtual on the base method

Don't
class Shape { public double Area() => 0; }
class Circle : Shape { public override double Area() => 3.14; }   // error CS0506: not marked virtual
Do
class Shape { public virtual double Area() => 0; }
class Circle : Shape { public override double Area() => 3.14; }

Not calling the base constructor

Don't
class Dog : Animal
{
    public Dog(string name) { }       // error CS7036: Animal has no parameterless constructor
}
Do
class Dog : Animal
{
    public Dog(string name) : base(name) { }
}

Inheriting just to reuse code

Don't
class Car : Engine { }                // a car isn't an engine
Do
class Car
{
    public Engine Engine { get; } = new();
}

C# Object-Oriented Programming in Real Projects

Inheritance in .NET

ASP.NET Core controllers inherit from ControllerBase to get helper methods like Ok() and NotFound(). Unity scripts inherit from MonoBehaviour and override methods such as Start and Update. Exceptions form a hierarchy, with your own exceptions inheriting from Exception. Domain models use abstract base classes for shared behaviour, such as an Entity base with an Id.

C# Object-Oriented Programming FAQ

What is the difference between virtual and abstract in C#?

A virtual method has a body that derived classes may override if they want. An abstract method has no body, can only appear in an abstract class, and every non-abstract derived class must override it. Use abstract when there's no sensible default behaviour.

Does C# support multiple inheritance?

Not for classes: a class can have only one base class. It can, however, implement any number of interfaces, such as class Report : Document, IPrintable, IExportable. Combining one base class with several interfaces, or using composition, covers the cases where other languages use multiple inheritance.

How do I call the base class constructor in C#?

Add : base(arguments) after the derived constructor's parameter list, as in public Dog(string name) : base(name) { }. The base constructor runs first, then the body of the derived one. If the base class has no parameterless constructor and you leave this out, the compiler reports an error.

What is the difference between is and as in C#?

is tests whether an object has a type and returns a bool; with a pattern, if (shape is Circle c), it also gives you a typed variable. as tries the conversion and returns null if it fails, so you must check for null afterwards. Modern C# code mostly uses is with a pattern.

What does sealed mean in C#?

A sealed class can't be used as a base class, so nobody can inherit from it; string is sealed, for example. On a method, sealed override stops classes further down the hierarchy from overriding it again. Sealing is useful when a class wasn't designed to be extended safely.

C# Object-Oriented Programming Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration so Cat inherits from Animal.

class Cat  Animal
{
}
Multiple choiceeasy

How many base classes can a C# class inherit from?

Predict the outputmedium

What does this print?

A obj = new B();
Console.WriteLine(obj.Name());
class A { public virtual string Name() => "A"; }
class B : A { public override string Name() => "B"; }
Multiple choicemedium

What does : base(name) on a constructor do?

Multiple choicehard

What must a non-abstract class do when it inherits an abstract method?

C# Object-Oriented Programming Quiz

5 questions to check what you learned.

  1. Which keyword allows a method to be overridden?

    • override
    • sealed
    • static
    • virtual
  2. Can you create an instance of an abstract class with new?

    • No
    • Yes
    • Only in the same file
    • Only with a primary constructor
  3. What does sealed on a class mean?

    • It can't have methods
    • It's read-only
    • No class can inherit from it
    • It's abstract
  4. What is polymorphism?

    • Treating different derived objects through a base type, with each running its own version of a method
    • Having many constructors
    • Copying objects
    • Using many namespaces
  5. Which class does every C# class ultimately inherit from?

    • Base
    • object
    • Class
    • Root