Inheritance

Inheritance lets a new class build on an existing one. The derived class (or subclass) automatically gets the members of its base class and can add new ones. It models "is a" relationships — a savings account is a bank account, a car is a vehicle — and lets you write shared code once.

Deriving a class

Put a colon and the base class name after the new class's name:

C#
var car = new Car { Brand = "Volvo", Seats = 5 };
car.Start();
car.Honk();
Console.WriteLine($"{car.Brand}, {car.Seats} seats");

class Vehicle
{
    public string Brand { get; set; } = "";
    public void Start() => Console.WriteLine($"{Brand} engine started");
}

class Car : Vehicle
{
    public int Seats { get; set; }
    public void Honk() => Console.WriteLine("Beep beep!");
}
Output
Volvo engine started
Beep beep!
Volvo, 5 seats

Car has everything Vehicle has (Brand, Start) plus its own Seats and Honk. A class can have only one base class, but there's no limit to how deep the chain goes.

A derived object is also a base object

Anywhere a Vehicle is expected, you can pass a Car. Through a Vehicle variable you can only use Vehicle members, even though the object is really a car:

C#
Vehicle v = new Car { Brand = "Kia", Seats = 7 };
v.Start();
Console.WriteLine(v is Car);
Console.WriteLine(v.GetType().Name);

if (v is Car c)
{
    Console.WriteLine($"It's a car with {c.Seats} seats");
}

class Vehicle
{
    public string Brand { get; set; } = "";
    public void Start() => Console.WriteLine($"{Brand} engine started");
}

class Car : Vehicle
{
    public int Seats { get; set; }
}
Output
Kia engine started
True
Car
It's a car with 7 seats

v is Car c tests the object's real type and, if it matches, gives you a variable c of type Car.

Constructors and base

Constructors aren't inherited. A derived class's constructor must make sure the base part is initialised, by calling a base constructor with : base(...):

C#
var s = new SavingsAccount("Lena", 1000m, 0.03m);
s.AddInterest();
Console.WriteLine(s);

class Account
{
    public string Owner { get; }
    public decimal Balance { get; protected set; }

    public Account(string owner, decimal balance)
    {
        Owner = owner;
        Balance = balance;
        Console.WriteLine("Account constructor");
    }

    public override string ToString() => $"{Owner}: {Balance:C}";
}

class SavingsAccount : Account
{
    public decimal Rate { get; }

    public SavingsAccount(string owner, decimal balance, decimal rate) : base(owner, balance)
    {
        Rate = rate;
        Console.WriteLine("SavingsAccount constructor");
    }

    public void AddInterest() => Balance += Balance * Rate;
}
Output
Account constructor
SavingsAccount constructor
Lena: $1,030.00

The base constructor always runs first. Balance has a protected set, so the derived class can update it but outside code can't.

Calling base members

base.Member calls the base class's version of a member — useful when the derived class adds to the base behaviour rather than replacing it:

C#
var m = new Manager("Iris", 5200m, 800m);
Console.WriteLine(m.Describe());

class Employee(string name, decimal salary)
{
    public string Name { get; } = name;
    public decimal Salary { get; } = salary;
    public virtual string Describe() => $"{Name} earns {Salary:C}";
}

class Manager(string name, decimal salary, decimal bonus) : Employee(name, salary)
{
    public decimal Bonus { get; } = bonus;
    public override string Describe() => base.Describe() + $" plus a {Bonus:C} bonus";
}
Output
Iris earns $5,200.00 plus a $800.00 bonus

With primary constructors, the base constructor arguments go straight after the base class name. virtual and override are the subject of the next lesson.

Everything derives from object

Every class ultimately derives from System.Object, which is where ToString, Equals, GetHashCode and GetType come from:

C#
object[] things = [42, "text", new Puppy(), 3.5];
foreach (object o in things)
{
    Console.WriteLine($"{o.GetType().Name,-7} base: {o.GetType().BaseType?.Name}");
}

class Animal { }
class Puppy : Animal { }
Output
Int32   base: ValueType
String  base: Object
Puppy   base: Animal
Double  base: ValueType

sealed

A sealed class can't be inherited from. Seal classes that weren't designed for extension — it keeps your options open and lets the runtime optimise calls. string is sealed, for example.

C#
sealed class Config { }
class MyConfig : Config { }
Build error
Program.cs(2,18): error CS0509: 'MyConfig': cannot derive from sealed type 'Config'

Hiding members (and why to avoid it)

If a derived class declares a member with the same name as a non-virtual base member, it hides it. The compiler warns, because which version runs then depends on the variable's type — a common source of confusion:

C#
Base b = new Derived();
b.Hello();
((Derived)b).Hello();

class Base { public void Hello() => Console.WriteLine("Base hello"); }
class Derived : Base { public void Hello() => Console.WriteLine("Derived hello"); }
Output
Program.cs(6,36): warning CS0108: 'Derived.Hello()' hides inherited member 'Base.Hello()'. Use the new keyword if hiding was intended.
Base hello
Derived hello

If you want derived classes to change behaviour, make the base member virtual and override it — lesson 37.

Inheritance or composition?

Inheritance is powerful but tightly couples classes. Use it when the "is a" relationship is genuine and stable. When one class merely uses another ("a car has an engine"), give it a field of that type instead — this is called composition, and it's usually more flexible. Interfaces (lesson 39) are another way to share a contract without sharing an implementation.

Try it yourself

Add a Truck class derived from Vehicle with a CargoTonnes property, and override Describe to include it.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Bike with 2 wheels
Lorry with 6 wheels, carries 18.5 t