Free Handbook · Every example compiled & verified

Inheritance & Interfaces

Inheritance, virtual and override, abstract and sealed classes, interfaces, type patterns and polymorphism in C#, and when composition beats all of it.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Derive a class from a base class, call the base constructor and reuse or extend base behaviour with base
  • Predict which method runs with virtual/override versus new (hiding), from the object's real type
  • Design with abstract classes, sealed classes and interfaces, including default and explicit interface members
  • Test and convert types safely with is, as and switch patterns instead of casts that throw
  • Recognise when composition is a better design than a deeper inheritance tree
01

Base classes, derived classes and base

Inheritance lets a class reuse and extend another. class Dog : Animal says a Dog is an Animal: it gets every public and protected member of Animal (the base class) and adds its own. C# allows one base class per class (single inheritance). A derived class's constructor must run a base constructor first; : base(...) chooses which one and what to pass.

C#Program.cs
var rex = new Dog("Rex", "beagle");
Console.WriteLine(rex.Describe());
rex.Eat();
rex.Fetch();

Animal a = rex;                  // a Dog IS an Animal
Console.WriteLine(a.Name);

class Animal
{
    public string Name { get; }
    protected int Meals;         // visible to derived classes, not to callers

    public Animal(string name) => Name = name;

    public void Eat()
    {
        Meals++;
        Console.WriteLine($"{Name} eats (meal {Meals})");
    }

    public string Describe() => $"{Name} the {GetType().Name}";
}

class Dog : Animal
{
    public string Breed { get; }

    public Dog(string name, string breed) : base(name)   // run Animal's constructor first
    {
        Breed = breed;
    }

    public void Fetch() => Console.WriteLine($"{Name} the {Breed} fetches after {Meals} meal(s)");
}
Outputcompiled & run with real C#
Rex the Dog
Rex eats (meal 1)
Rex the beagle fetches after 1 meal(s)
Rex
Your turn

Add a Cat : Animal with a Purr() method. Can you call a.Fetch() on the Animal a variable? Try it and read the error.

Everything inherits from object
A class with no base class listed derives from System.Object (object). That is where ToString, Equals, GetHashCode and GetType come from (Module 05).
Error you will hit

CS7036: the base class has no parameterless constructor

C#
var d = new Dog("beagle");

class Animal
{
    public string Name { get; }
    public Animal(string name) => Name = name;
}

class Dog : Animal
{
    public string Breed { get; }
    public Dog(string breed) => Breed = breed;
}
Program.cs(12,12): error CS7036: There is no argument given that corresponds to the required parameter 'name' of 'Animal.Animal(string)'
Why the compiler said that

Without : base(...), C# calls the base class's parameterless constructor. Animal has none: it insists on a name. So the compiler reports that nothing supplies name.

The fix

Chain to the base constructor explicitly and pass what it needs.

C#
var d = new Dog("Rex", "beagle");
Console.WriteLine($"{d.Name} {d.Breed}");

class Animal
{
    public string Name { get; }
    public Animal(string name) => Name = name;
}

class Dog : Animal
{
    public string Breed { get; }
    public Dog(string name, string breed) : base(name) => Breed = breed;
}
02

virtual, override and new: overriding versus hiding

A base class marks a method virtual to say "derived classes may replace this". A derived class replaces it with override. From then on, the method that runs is chosen by the real type of the object at runtime, not by the type of the variable: an Animal variable holding a Cat runs the cat's version. This is dynamic dispatch, and it is what makes polymorphism work. Inside an override, base.Method() calls the version being replaced.

new on a method does something different: it hides the base method rather than overriding it. The choice is then made by the variable's declared type at compile time. Hiding is almost always a mistake, and C# warns (CS0108) if you hide without writing new.

VisualizeWhich Speak runs?Step 1 / 7
Animal a = new Cat();
Console.WriteLine(a.Speak());
Console.WriteLine(a.Kind());
Cat c = (Cat)a;
Console.WriteLine(c.Kind());
class Animal
{
public virtual string Speak() => "...";
public string Kind() => "animal";
}
class Cat : Animal
{
public override string Speak() => base.Speak() + "meow";
public new string Kind() => "cat";
}
Line 1

The object is a Cat; the variable's declared type is Animal.

Variables now
aAnimal variable -> Cat object
All 7 steps as a table
StepLineWhat happenedVariables now
11The object is a Cat; the variable's declared type is Animal.a = Animal variable -> Cat object
22Speak is virtual, so the runtime looks at the object: it is a Cat, and Cat overrides Speak.a = Animal variable -> Cat object
315Cat.Speak runs and first calls base.Speak(), the Animal version, which returns "...".result = "...meow"
42The override's result is printed.
53Kind is not virtual. Cat's new Kind only hides it, so the compiler binds to the declared type, Animal.
64A cast gives a variable whose declared type is Cat. Same object.c = Cat variable -> same Cat object
75Now the declared type is Cat, so the hiding method is chosen.
C#Program.cs
var shapes = new List<Shape> { new Circle(1), new Square(2), new Shape() };
foreach (Shape s in shapes)
    Console.WriteLine($"{s.Name}: {s.Area():0.00}");

class Shape
{
    public virtual string Name => "shape";
    public virtual double Area() => 0;
}

class Circle(double r) : Shape
{
    public override string Name => "circle";
    public override double Area() => Math.PI * r * r;
}

class Square(double side) : Shape
{
    public override string Name => $"square ({base.Name} with 4 equal sides)";
    public override double Area() => side * side;
}
Outputcompiled & run with real C#
circle: 3.14
square (shape with 4 equal sides): 4.00
shape: 0.00

Properties can be virtual too. The loop only knows it has Shapes; each object supplies its own Name and Area.

Error you will hit

CS0506: overriding a method that is not virtual

C#
Animal pet = new Cat();
Console.WriteLine(pet.Speak());

class Animal
{
    public string Speak() => "...";
}

class Cat : Animal
{
    public override string Speak() => "meow";
}
Program.cs(11,28): error CS0506: 'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override
Why the compiler said that

In C#, methods are not virtual unless the base class says so (the opposite of Java). override needs a base member marked virtual, abstract or override to replace. The base author decides what may be customised.

The fix

Mark the base method virtual if it is meant to be overridden. If you do not own the base class, you cannot override it; wrap it instead (see composition below).

C#
Animal pet = new Cat();
Console.WriteLine(pet.Speak());

class Animal
{
    public virtual string Speak() => "...";
}

class Cat : Animal
{
    public override string Speak() => "meow";
}
In real jobs
Code reviewers treat new on a method as a red flag. The legitimate case is a library that later adds a member with the same name as one of yours; new then documents that the clash is deliberate. If you meant polymorphism, you meant virtual + override.
03

Abstract classes and sealed classes

An abstract class is an incomplete base class: you cannot create one with new, only derive from it. It can declare abstract members with no body, and every non-abstract derived class must override them. It can also have ordinary fields, constructors and finished methods. Use one when related types share real code and state but each must fill in a few specific steps.

C#Program.cs
var jobs = new List<Report> { new SalesReport(), new StockReport() };
foreach (var job in jobs)
    Console.WriteLine(job.Render());

abstract class Report
{
    // the template: fixed order, specific steps left to subclasses
    public string Render() => $"== {Title} ==\n{Body()}\n-- {Footer} --";

    protected abstract string Title { get; }
    protected abstract string Body();
    protected virtual string Footer => "end";      // optional to override
}

class SalesReport : Report
{
    protected override string Title => "Sales";
    protected override string Body() => "total: 1,200";
}

sealed class StockReport : Report
{
    protected override string Title => "Stock";
    protected override string Body() => "items: 37";
    protected override string Footer => "checked";
}
Outputcompiled & run with real C#
== Sales ==
total: 1,200
-- end --
== Stock ==
items: 37
-- checked --

This shape (a base method fixing the order, abstract steps filled in by subclasses) is the Template Method pattern.

sealed is the opposite: a sealed class cannot be derived from. string is sealed. Seal classes you did not design for extension; it keeps your options open and lets the JIT call their methods without a virtual lookup. You can also seal a single override (sealed override) to stop further overriding below that point.

Error you will hit

CS0534: not implementing an abstract member

C#
Report r = new SalesReport();

abstract class Report
{
    protected abstract string Title { get; }
    protected abstract string Body();
}

class SalesReport : Report
{
    protected override string Title => "Sales";
}
Program.cs(9,7): error CS0534: 'SalesReport' does not implement inherited abstract member 'Report.Body()'
Why the compiler said that

SalesReport is not abstract, so it must be complete: every abstract member it inherits needs an override. It supplies Title but not Body().

The fix

Override every abstract member, or mark SalesReport itself abstract if it is meant to be another partial base.

C#
Report r = new SalesReport();
Console.WriteLine(r.GetType().Name);

abstract class Report
{
    protected abstract string Title { get; }
    protected abstract string Body();
}

class SalesReport : Report
{
    protected override string Title => "Sales";
    protected override string Body() => "total: 1,200";
}
Error you will hit

CS0144: creating an instance of an abstract class

C#
var r = new Report();

abstract class Report
{
    public abstract string Body();
}
Program.cs(1,9): error CS0144: Cannot create an instance of the abstract type or interface 'Report'
Why the compiler said that

An abstract class is unfinished by definition: Body() has no code, so there is nothing complete to create. Only concrete derived classes can be instantiated, though a variable may still be typed as the abstract class.

The fix

Create a concrete derived class and hold it in a variable of the abstract type if you like.

C#
Report r = new SalesReport();
Console.WriteLine(r.Body());

abstract class Report
{
    public abstract string Body();
}

class SalesReport : Report
{
    public override string Body() => "total: 1,200";
}
Error you will hit

CS0509: deriving from a sealed class

C#
Console.WriteLine(new LoudString().Shout("hi"));

class LoudString : string
{
    public string Shout(string s) => s.ToUpper() + "!";
}
Program.cs(3,20): error CS0509: 'LoudString': cannot derive from sealed type 'string'
Why the compiler said that

string is sealed: its authors closed it to extension, so no class may list it as a base. The same applies to any class you mark sealed.

The fix

Add behaviour without inheriting: an extension method (or a static helper) works on every string, and composition (holding a value in a field) covers the rest.

C#
Console.WriteLine("hi".Shout());

static class StringExtensions
{
    public static string Shout(this string s) => s.ToUpper() + "!";
}
04

Interfaces: contracts, multiple interfaces and default methods

An interface is a contract: a list of members a type promises to have, with no state. interface IShape { double Area(); } says "anything that is an IShape can compute its area". A class has only one base class but can implement any number of interfaces, and structs and records can implement them too. By convention interface names start with I.

C#Program.cs
var items = new List<object> { new Photo("beach.jpg", 2400), new Invoice(99.5m) };

foreach (var item in items)
{
    if (item is IPrintable p) Console.WriteLine(p.Print());
    if (item is ISized s) Console.WriteLine($"  size: {s.Describe()}");
}

interface IPrintable
{
    string Print();
}

interface ISized
{
    long Bytes { get; }
    // default interface method: implementers get it for free
    string Describe() => Bytes >= 1024 ? $"{Bytes / 1024} KB" : $"{Bytes} B";
}

class Photo(string file, long bytes) : IPrintable, ISized   // two interfaces
{
    public long Bytes => bytes;
    public string Print() => $"[photo {file}]";
}

class Invoice(decimal total) : IPrintable
{
    public string Print() => $"[invoice {total:0.00}]";
}
Outputcompiled & run with real C#
[photo beach.jpg]
  size: 2 KB
[invoice 99.50]
Your turn

Give Invoice an ISized implementation too (say, 300 bytes) and check the new output.

Default methods are reached through the interface
A default interface method is not inherited as a class member. photo.Describe() on a Photo variable does not compile; ((ISized)photo).Describe() or a variable typed ISized does. Default methods exist mostly so library authors can add a member to a published interface without breaking every implementer.

Explicit interface implementation

When two interfaces declare a member with the same name, or you want a member to exist only when the object is viewed through the interface, implement it explicitly: write string IA.Name => ... with no access modifier. That member is then callable only through a variable of the interface type.

C#Program.cs
var doc = new Document();
Console.WriteLine(((IEnglish)doc).Title);
Console.WriteLine(((IFrench)doc).Title);

IFrench fr = doc;
Console.WriteLine(fr.Title);

interface IEnglish { string Title { get; } }
interface IFrench { string Title { get; } }

class Document : IEnglish, IFrench
{
    string IEnglish.Title => "User guide";
    string IFrench.Title => "Guide de l'utilisateur";
}
Outputcompiled & run with real C#
User guide
Guide de l'utilisateur
Guide de l'utilisateur
Error you will hit

CS0535: a class that does not implement its interface

C#
IShape c = new Circle(1);
Console.WriteLine(c.Area());

interface IShape
{
    double Area();
    double Perimeter();
}

class Circle(double r) : IShape
{
    public double Area() => Math.PI * r * r;
}
Program.cs(10,26): error CS0535: 'Circle' does not implement interface member 'IShape.Perimeter()'
Why the compiler said that

Listing IShape is a promise to provide every member it declares (apart from those with default bodies). Perimeter is missing, so Circle is not really an IShape.

The fix

Implement the missing member. Your editor's "Implement interface" quick fix generates the stubs.

C#
IShape c = new Circle(1);
Console.WriteLine($"{c.Area():0.00} {c.Perimeter():0.00}");

interface IShape
{
    double Area();
    double Perimeter();
}

class Circle(double r) : IShape
{
    public double Area() => Math.PI * r * r;
    public double Perimeter() => 2 * Math.PI * r;
}

Abstract class

  • One per class (single inheritance)
  • Can hold fields, constructors and state
  • Shares real implementation between close relatives
  • Models "is a kind of"

Interface

  • A class can implement many
  • No instance fields; default methods only
  • Works for classes, structs and records alike
  • Models "can do": IComparable, IDisposable, IEnumerable
05

is, as, casts and pattern matching on types

Sometimes you hold a base-typed reference and need the derived type. There are three tools. A cast (Dog)animal says "I am sure": if you are wrong it throws InvalidCastException. as says "try": animal as Dog gives null instead of throwing (reference types only). The is type pattern tests and declares a typed variable in one step, if (animal is Dog d), and is what modern C# uses almost everywhere.

C#Program.cs
object[] things = { 42, "hello", 3.5, new Dog("Rex"), null!, new List<int> { 1, 2 } };

foreach (object? t in things)
    Console.WriteLine(Describe(t));

static string Describe(object? o) => o switch
{
    null => "nothing",
    int n when n > 40 => $"a big int {n}",
    int n => $"an int {n}",
    string { Length: > 3 } s => $"a long string \"{s}\"",
    Dog d => $"a dog called {d.Name}",
    IEnumerable<int> seq => $"ints summing to {seq.Sum()}",
    _ => $"something else: {o.GetType().Name}",
};

record Dog(string Name);
Outputcompiled & run with real C#
a big int 42
a long string "hello"
something else: Double
a dog called Rex
nothing
ints summing to 3

Arms are tried top to bottom. int n when n > 40 adds a guard; string { Length: > 3 } is a property pattern. Switch expressions were introduced in Module 02.

C#Program.cs
Animal pet = new Cat();

Dog? maybeDog = pet as Dog;                 // no exception, just null
Console.WriteLine(maybeDog is null ? "not a dog" : "a dog");

if (pet is Cat cat)
    Console.WriteLine($"cat says {cat.Meow()}");

if (pet is not Dog)
    Console.WriteLine("definitely not a dog");

class Animal { }
class Dog : Animal { }
class Cat : Animal { public string Meow() => "meow"; }
Outputcompiled & run with real C#
not a dog
cat says meow
definitely not a dog
Error you will hit

InvalidCastException: a cast that is wrong at runtime

C#
Animal pet = new Cat();
Dog dog = (Dog)pet;
Console.WriteLine("unreachable");

class Animal { }
class Dog : Animal { }
class Cat : Animal { }
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'Cat' to type 'Dog'.
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

The compiler accepts the cast because an Animal might be a Dog. At runtime the object is a Cat, so the cast is impossible and the runtime throws. An explicit cast is you overruling the type checker, and you were wrong.

The fix

Test before you convert: if (pet is Dog dog). Better still, if you find yourself type-testing to decide behaviour, a virtual method on Animal usually removes the need.

C#
Animal pet = new Cat();
if (pet is Dog dog) Console.WriteLine("a dog");
else Console.WriteLine($"not a dog, a {pet.GetType().Name}");

class Animal { }
class Dog : Animal { }
class Cat : Animal { }
06

Polymorphism: one call, many behaviours

Polymorphism means code written against a base type or interface works with every type that derives from or implements it, each supplying its own behaviour. The caller does not ask "what are you?"; it just calls the method. Adding a new kind of thing means writing one new class, with no if/switch edits scattered through the code base. This is the open/closed principle in practice.

C#Program.cs
var checkout = new Checkout(new List<IDiscount>
{
    new PercentOff(10),
    new FixedOff(5m),
    new BuyOver(100m, 15m),
});

Console.WriteLine(checkout.Total(80m));
Console.WriteLine(checkout.Total(200m));

interface IDiscount
{
    decimal Apply(decimal price);
}

class PercentOff(int percent) : IDiscount
{
    public decimal Apply(decimal price) => price * (100 - percent) / 100;
}

class FixedOff(decimal amount) : IDiscount
{
    public decimal Apply(decimal price) => Math.Max(0, price - amount);
}

class BuyOver(decimal threshold, decimal amount) : IDiscount
{
    public decimal Apply(decimal price) => price >= threshold ? price - amount : price;
}

class Checkout(List<IDiscount> discounts)
{
    // knows nothing about the concrete discount types
    public decimal Total(decimal price)
    {
        foreach (var d in discounts) price = d.Apply(price);
        return price;
    }
}
Outputcompiled & run with real C#
67
160
Your turn

Add a class NoDiscountOnSunday : IDiscount (any rule you like) and pass it to Checkout. Which existing classes did you have to change?

In real jobs
This is how ASP.NET Core applications are wired. Services depend on interfaces (IEmailSender, IOrderRepository), and the dependency injection container hands in a real implementation in production and a fake in tests. Interfaces with one implementation are common and normal for exactly that reason.
07

Composition over inheritance

Inheritance couples a class tightly to its base: every change to the base ripples down, and a class can only have one. Deep trees (Vehicle > Car > ElectricCar > ElectricSportsCar) break down the first time something needs features from two branches. Composition builds a class from objects it has (usually behind interfaces) instead of a class it is. Behaviours can then be mixed, swapped and tested on their own.

C#Program.cs
var cheap = new Car("city car", new PetrolEngine(), new BasicStereo());
var fancy = new Car("tourer", new ElectricMotor(), new PremiumStereo());

cheap.Drive();
fancy.Drive();

interface IEngine { string Start(); }
interface IStereo { string Play(); }

class PetrolEngine : IEngine { public string Start() => "vroom"; }
class ElectricMotor : IEngine { public string Start() => "hum"; }
class BasicStereo : IStereo { public string Play() => "radio"; }
class PremiumStereo : IStereo { public string Play() => "surround sound"; }

// Car HAS an engine and a stereo; it does not inherit from either
class Car(string model, IEngine engine, IStereo stereo)
{
    public void Drive() =>
        Console.WriteLine($"{model}: {engine.Start()}, playing {stereo.Play()}");
}
Outputcompiled & run with real C#
city car: vroom, playing radio
tourer: hum, playing surround sound

Four engine and stereo combinations need no new classes. With inheritance you would need a subclass per combination.

  • Use inheritance for a genuine, stable "is a" relationship where the base is designed for it: framework base classes (Exception, ControllerBase), a small closed family of shapes.
  • Use composition when you want to reuse behaviour, when the behaviour could change at runtime, or when you would otherwise need multiple inheritance.
  • If you are overriding a method just to throw NotSupportedException, the "is a" was false. Compose instead.
Base class / derived class
The class inherited from, and the class that inherits (class Dog : Animal).
base
Keyword to call the base constructor (: base(x)) or the base version of a member (base.Speak()).
virtual / override
Mark a member replaceable, and replace it; the object's runtime type picks the version (dynamic dispatch).
new (member hiding)
Hides a base member instead of overriding it; the variable's declared type picks the version.
Abstract class
A class that cannot be instantiated and may declare abstract members derived classes must implement.
Sealed
A class that cannot be derived from, or an override that cannot be overridden further.
Interface
A contract of members with no instance state; a type can implement many.
Explicit implementation
An interface member callable only through the interface type (string IA.Name => ...).
Type pattern
x is Dog d or a switch arm Dog d =>: tests a type and binds a typed variable.
Polymorphism
One call site working with many types, each supplying its own behaviour.
Composition
Building a class from objects it holds rather than from a class it inherits.
C#
class Base { public virtual string A() => "base A"; public string B() => "base B"; }
class Derived : Base { public override string A() => "derived A"; public new string B() => "derived B"; }

Base x = new Derived();
Console.WriteLine(x.A() + ", " + x.B());
Quick check

What does the last line of the code above print?

Quick check

You need to convert an object to Customer and do something only if it is one. Which is the idiomatic C#?

Frequently asked questions

What is the difference between an abstract class and an interface in C#?
An abstract class can hold state, constructors and shared implementation, but a class can inherit only one. An interface is a contract with no instance state that a class, struct or record can implement many of. Use an abstract class for closely related types sharing code, and interfaces for capabilities.
What is the difference between override and new in C#?
override replaces a virtual base method, so the runtime type of the object decides which version runs. new hides the base method, so the compile-time type of the variable decides. Use override for polymorphism; new is rarely what you want.
Does C# support multiple inheritance?
Not of classes: a class has exactly one base class. It can implement any number of interfaces, and interfaces can carry default method implementations, which covers most needs. For reusing behaviour from several places, use composition.

Finish the C# handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.