Inheritance & Polymorphism

Reviewed & published by Brayan K

By the end of this lesson you'll be able to build a family of related classes that share code through inheritance, replace behaviour with virtual/override, and write one piece of code that works for every type via polymorphism — the OOP pillars that keep large programs flexible.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Inheritance is like biology. "Dog", "Cat" and "Bird" are all specialised kinds of "Animal": they inherit the general traits (a name, the ability to eat) and then add or change their own (a dog barks, a bird flies). You write the shared parts once in the base class instead of copying them into every animal. Polymorphism is like a universal remote's "play" button — you press the same button, but it does the right thing depending on whether a TV, a speaker, or a games console is listening. In code, you call Speak() on an Animal and each object responds in its own way.

📋 Inheritance Keywords Reference

KeywordWhere it goesWhat it does
:Class headerclass Dog : Animal — Dog inherits Animal
: base(...)ConstructorCalls the parent's constructor
virtualBase methodAllows derived classes to override it
overrideDerived methodReplaces the base implementation
base.X()Derived methodCalls the parent's version of a method
abstractClass / methodCannot be instantiated; must be implemented
sealedClass / methodPrevents any further inheritance/override
isExpressionif (a is Dog d) — check & cast
asExpressiona as Cat — cast, or null on failure

1. Base & Derived Classes

A base class (the parent) holds traits shared by a whole family of things. A derived class (the child) is written with class Child : Parent and automatically gets every public field, property and method of the parent — you don't rewrite them. The phrase to remember is "is a": a Car is a Vehicle, so Car : Vehicle is correct. When the parent has a constructor with parameters, the child must pass them up with : base(...). The base constructor runs first, then the child's. Read this worked example, run it, then you'll write your own.

using System;

// Base class (the "parent") — holds traits shared by ALL vehicles.
class Vehicle
{
    public string Brand { get; set; }
    public int Year { get; set; }

    // Constructor: sets the shared fields when a Vehicle is built.
    public Vehicle(string brand, int year)
    {
        Brand = brand;
        Year = year;
    }

    public void Start()
    {
        Console.WriteLine($"{Brand} ({Year}) — engine started!");
    }
}

// Derived class (the "child"). " : Vehicle " means "Car IS A Vehicle".
class Car : Vehicle
{
    public int Doors { get; set; }

    // : base(brand, year) passes data UP to the Vehicle constructor.
    public Car(string brand, int year, int doors) : base(brand, year)
    {
        Doors = doors;
    }

    public void OpenBoot()
    {
        Console.WriteLine($"{Brand} boot opened.");
    }
}

class Program
{
    static void Main()
    {
        var car = new Car("Toyota", 2024, 4);
        car.Start();          // Inherited from Vehicle  -> Toyota (2024) — engine started!
        car.OpenBoot();       // Car's own method         -> Toyota boot opened.
        Console.WriteLine($"Doors: {car.Doors}");  // Doors: 4
    }
}

// ✅ Expected output:
//    Toyota (2024) — engine started!
//    Toyota boot opened.
//    Doors: 4

Your turn. The Dog class is already written for you at the bottom. Fill in the two blanks marked ___ in Main so Dog is recognised as an Animal and you call the method it inherited.

using System;

class Animal
{
    public string Name { get; set; }

    public Animal(string name) { Name = name; }

    public void Eat()
    {
        Console.WriteLine($"{Name} is eating.");
    }
}

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — make Dog inherit from Animal, then call the inherited method.

        // 1) Make Dog a derived class of Animal (Dog IS AN Animal)
        //    👉 fill the blank with the base class name
        // class Dog : ___        // 👉 replace ___ with  Animal

        var rex = new Dog("Rex");

        // 2) Call the method Dog INHERITED from Animal
        rex.___();                // 👉 replace ___ with  Eat

        rex.Fetch();              // Dog's own method (already written below)

        // ✅ Expected output:
        //    Rex is eating.
        //    Rex fetches the ball!
    }
}

// This class is finished for you — DON'T edit it.
class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public void Fetch()
    {
        Console.WriteLine($"{Name} fetches the ball!");
    }
}

2. Overriding with virtual & override

Inheriting a method gives you the parent's behaviour, but often a child needs to do something different. Mark the base method virtual ("you may replace me") and the child's version override ("I'm replacing it"). The magic part: even when the object is stored in a variable typed as the base class, C# runs the actual object's override at runtime. That is polymorphism — "many shapes" — one method name, the right behaviour for each type.

using System;

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

    // virtual = "derived classes are ALLOWED to override this method".
    public virtual void Speak()
    {
        Console.WriteLine($"{Name} makes a sound.");
    }
}

class Dog : Animal
{
    public Dog(string name) : base(name) { }

    // override = "replace the base version when called on a Dog".
    public override void Speak()
    {
        Console.WriteLine($"{Name}: Woof!");
    }
}

class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void Speak() => Console.WriteLine($"{Name}: Meow!");
}

class Program
{
    static void Main()
    {
        // Each variable is typed as the BASE class (Animal)...
        Animal a1 = new Dog("Rex");
        Animal a2 = new Cat("Whiskers");
        Animal a3 = new Animal("Generic");

        // ...but C# runs the ACTUAL object's override (this is polymorphism).
        a1.Speak();   // Rex: Woof!          (Dog's override)
        a2.Speak();   // Whiskers: Meow!     (Cat's override)
        a3.Speak();   // Generic makes a sound.  (no override -> base version)
    }
}

// ✅ Expected output:
//    Rex: Woof!
//    Whiskers: Meow!
//    Generic makes a sound.

Now you try. FrenchGreeter needs to override Greet(). Fill in the keyword that replaces the base method and the French greeting it should return.

using System;

class Greeter
{
    // virtual so a derived class can replace it.
    public virtual string Greet()
    {
        return "Hello.";
    }
}

class FrenchGreeter : Greeter
{
    // 🎯 YOUR TURN — override Greet() to return a French greeting.

    // 1) Add the keyword that REPLACES the base method
    public ___ string Greet()      // 👉 replace ___ with  override
    {
        // 2) Return a French hello
        return ___;                // 👉 replace ___ with  "Bonjour."
    }
}

class Program
{
    static void Main()
    {
        // Same base type, different runtime behaviour = polymorphism.
        Greeter g1 = new Greeter();
        Greeter g2 = new FrenchGreeter();   // base-typed reference, French object

        Console.WriteLine(g1.Greet());      // Hello.
        Console.WriteLine(g2.Greet());      // Bonjour.   (the override runs)

        // ✅ Expected output:
        //    Hello.
        //    Bonjour.
    }
}

🔎 Deep Dive: extend, don't copy, with base.Method()

Sometimes you don't want to replace the parent's method — you want to add to it. From inside an override, call base.MethodName() to run the parent's version, then do your extra work. This keeps the shared logic in one place instead of copying it into every child.

using System;

class Logger
{
    public virtual void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

class TimestampLogger : Logger
{
    public override void Log(string message)
    {
        // base.Log(...) calls the ORIGINAL version, then we add to it.
        Console.Write("2026-06-14 ");
        base.Log(message);     // reuse the parent's behaviour instead of copying it
    }
}

class Program
{
    static void Main()
    {
        Logger logger = new TimestampLogger();
        logger.Log("User signed in");   // 2026-06-14 [LOG] User signed in
    }
}

3. Polymorphism, is & as

Polymorphism really pays off when you treat a mixed bag of objects uniformly. Store different derived objects in one base-typed array (Animal[]), loop over them once, and each call lands on the correct override — no if/switch on the type. When you do need the specific type, is checks and casts in a single step (if (a is Dog dog)), and as attempts a cast that yields null instead of crashing if it fails.

using System;

class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }
    public virtual void Speak() => Console.WriteLine($"{Name} makes a sound.");
}

class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void Speak() => Console.WriteLine($"{Name}: Woof!");
    public void Fetch() => Console.WriteLine($"{Name} fetches the ball!");
}

class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void Speak() => Console.WriteLine($"{Name}: Meow!");
}

class Program
{
    static void Main()
    {
        // One array of the BASE type can hold any derived object.
        Animal[] animals = { new Dog("Rex"), new Cat("Whiskers"), new Dog("Buddy") };

        Console.WriteLine("=== Roll call ===");
        foreach (Animal animal in animals)
            animal.Speak();      // the correct override is chosen at runtime

        // 'is' checks the real type AND casts in one step (pattern matching).
        foreach (Animal animal in animals)
        {
            if (animal is Dog dog)        // true only for the Dogs
                dog.Fetch();              // 'dog' is safely typed as Dog here
        }

        // 'as' tries to cast; it returns null instead of throwing if it fails.
        Animal first = animals[0];
        Cat maybeCat = first as Cat;      // first is a Dog, so this is null
        Console.WriteLine(maybeCat is null ? "Not a cat" : "A cat");  // Not a cat
    }
}

// ✅ Expected output:
//    === Roll call ===
//    Rex: Woof!
//    Whiskers: Meow!
//    Buddy: Woof!
//    Rex fetches the ball!
//    Buddy fetches the ball!
//    Not a cat

4. Abstract Classes (vs Interfaces)

An abstract class is a base class you can never new directly — it only exists to be inherited. It can hold shared fields and finished methods, plus abstract methods that have no body and that every child must implement. That makes it a contract with some implementation baked in. An interface is a pure contract — method signatures only, no fields, and a class can implement many interfaces but inherit only one class. Rule of thumb: use an abstract class when relatives share state and code (Employee → Contractor); use an interface for a capability unrelated classes can share (IComparable, IDisposable).

using System;

// abstract = a class you CANNOT 'new' directly; it only exists to be inherited.
abstract class Employee
{
    public string Name { get; set; }
    public double BaseSalary { get; set; }

    public Employee(string name, double baseSalary)
    {
        Name = name;
        BaseSalary = baseSalary;
    }

    // Abstract method: NO body. Every derived class MUST implement it.
    public abstract double CalculatePay();

    // A normal method, inherited and shared by all employees.
    public void PrintPayslip()
    {
        Console.WriteLine($"{Name}: £{CalculatePay():F2}");
    }
}

class FullTimeEmployee : Employee
{
    public FullTimeEmployee(string name, double salary) : base(name, salary) { }
    public override double CalculatePay() => BaseSalary;
}

class Contractor : Employee
{
    public double Hours { get; set; }
    public Contractor(string name, double rate, double hours) : base(name, rate)
    {
        Hours = hours;
    }
    public override double CalculatePay() => BaseSalary * Hours;
}

class Program
{
    static void Main()
    {
        Employee[] staff =
        {
            new FullTimeEmployee("Alice", 3500),
            new Contractor("Bob", 75, 120)
        };

        Console.WriteLine("=== Payroll ===");
        foreach (var emp in staff)
            emp.PrintPayslip();   // Alice: £3500.00 / Bob: £9000.00

        // Employee e = new Employee("X", 0);  // ❌ won't compile — abstract!
    }
}

// ✅ Expected output:
//    === Payroll ===
//    Alice: £3500.00
//    Bob: £9000.00

🔎 Deep Dive: sealed — closing a class for inheritance

The opposite of abstract is sealed: it marks a class as final so nothing can inherit from it. Use it when a class is complete and you don't want subclasses changing its behaviour — it also lets the runtime optimise calls. You can also seal a single override to stop deeper classes overriding it again.

using System;

class Account
{
    public virtual void Describe() => Console.WriteLine("A bank account.");
}

// sealed = "this class is final — no one may inherit from it".
sealed class SavingsAccount : Account
{
    public override void Describe() => Console.WriteLine("A savings account.");
}

// class PremiumSavings : SavingsAccount { }  // ❌ CS0509: cannot derive from sealed

class Program
{
    static void Main()
    {
        Account acc = new SavingsAccount();
        acc.Describe();   // A savings account.
    }
}

// ✅ Expected output:
//    A savings account.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Inherit a classclass Dog : AnimalDog "is a" Animal
Call parent constructor: base(name)Runs before child body
Allow overridingpublic virtual void X()On the base method
Replace behaviourpublic override void X()On the derived method
Reuse parent methodbase.X()Inside the override
Must-implement methodpublic abstract void X();In an abstract class
Check & castif (a is Dog d)d is typed Dog inside
Safe casta as Catnull if it isn't a Cat

Frequently Asked Questions

Q: What's the difference between override and new on a method?

override truly replaces the base method, so polymorphism works — a base-typed reference calls your version. new only hides the base method; which one runs depends on the variable's declared type, not the real object. You almost always want override.

Q: When should I use an abstract class instead of an interface?

Use an abstract class when the children are genuine relatives that share fields and some finished code (Employee → Manager). Use an interface for a capability unrelated classes can share (e.g. anything "comparable" or "disposable"). A class can implement many interfaces but inherit from only one class.

Q: Why must I write : base(...) sometimes but not always?

If the parent only has a constructor that takes arguments, the child must supply them with : base(...). If the parent has a parameterless constructor, C# calls it for you automatically and you can omit : base().

Q: Is is better than casting with (Dog)animal?

Yes, for safety. A direct cast throws InvalidCastException if the object isn't that type. if (animal is Dog dog) checks first and only enters the block when the cast is valid, and as returns null instead of throwing — both avoid crashes.

Mini-Challenge: Shape Areas

No blanks this time — just a brief and an outline. Build a small shape hierarchy, override Area() in each shape, then loop over a Shape[] and print every area. This is the exact pattern real graphics, billing and reporting code uses.

using System;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: Areas with polymorphism
        //
        // 1. Make a BASE class  Shape  with a  virtual double Area()
        //    that returns 0.
        // 2. Make  Circle : Shape  (field: double Radius) that OVERRIDES Area()
        //    to return  Math.PI * Radius * Radius.
        // 3. Make  Rectangle : Shape (fields: double Width, double Height) that
        //    OVERRIDES Area() to return  Width * Height.
        // 4. Put a Circle(2) and a Rectangle(3, 4) into a  Shape[]  array.
        // 5. foreach over the array and print  $"Area: {s.Area():F2}".
        //
        // ✅ Expected output:
        //    Area: 12.57
        //    Area: 12.00

        // your code here
    }
}

// Define your Shape, Circle and Rectangle classes here (outside Main).

🎉 Lesson Complete

Practice quiz

How do you make Dog inherit from Animal?

  • class Dog extends Animal
  • class Dog -> Animal
  • class Dog : Animal
  • class Dog inherits Animal

Answer: class Dog : Animal. C# uses a colon: class Dog : Animal means Dog is a derived class of Animal.

What does : base(name) in a derived constructor do?

  • Passes arguments up to the base class constructor
  • Creates a new field
  • Calls a static method
  • Overrides a method

Answer: Passes arguments up to the base class constructor. : base(...) passes data to the parent constructor, which runs before the child's body.

A method can only be overridden if the base marks it with which keyword?

  • override
  • sealed
  • static
  • virtual (or abstract)

Answer: virtual (or abstract). The base method must be virtual (or abstract) before a derived class may override it.

Which keyword does the derived class use to replace a virtual method?

  • virtual
  • override
  • new
  • base

Answer: override. override on the derived method replaces the base implementation and enables polymorphism.

What does calling base.Log(message) inside an override do?

  • Runs the parent's version of the method
  • Calls the child's version
  • Throws an error
  • Skips the method

Answer: Runs the parent's version of the method. base.Method() runs the parent's implementation so you can extend it instead of copying it.

When Animal a1 = new Dog("Rex"); calls a1.Speak(), whose version runs?

  • Animal's, because the variable is typed Animal
  • Neither — it errors
  • Dog's override, chosen at runtime
  • Both

Answer: Dog's override, chosen at runtime. Polymorphism: C# runs the actual object's override (Dog's) even through a base-typed reference.

What is true about an abstract class?

  • You can create it with new directly
  • It cannot be instantiated and exists only to be inherited
  • It cannot have any methods
  • It is the same as sealed

Answer: It cannot be instantiated and exists only to be inherited. An abstract class can't be newed up directly; it serves as a base for derived classes.

What must every derived class do with an abstract method?

  • Ignore it
  • Call base on it
  • Seal it
  • Implement (override) it

Answer: Implement (override) it. An abstract method has no body, so each concrete derived class MUST implement it.

What does the sealed keyword on a class do?

  • Makes it abstract
  • Prevents any class from inheriting from it
  • Hides all its methods
  • Makes it static

Answer: Prevents any class from inheriting from it. sealed marks a class as final — nothing can derive from it (CS0509 if you try).

What does if (animal is Dog dog) do?

  • Always throws if not a Dog
  • Converts animal to a Dog forcibly
  • Checks the type and, if it matches, casts to dog in one step
  • Compares animal to the text "Dog"

Answer: Checks the type and, if it matches, casts to dog in one step. The is pattern tests the runtime type and safely assigns the cast value to dog only when it matches.

Continue this course