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:
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!"); }
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:
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; } }
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(...):
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; }
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:
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"; }
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:
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 { }
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.
sealed class Config { } class MyConfig : Config { }
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:
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"); }
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.
Bike with 2 wheels Lorry with 6 wheels, carries 18.5 t