← Open in the full interactive course (progress tracking, search & more)

Inheritance lets you build new classes on top of existing ones — reusing code, extending behavior, and modeling real‑world hierarchies.

Imagine you're building a system for a zoo. You have Animal — it has a name, age, and a method Eat(). Then you need Lion, Elephant, and Penguin. Each is an animal, but they have different behaviors: a lion Roar()s, an elephant Trumpet()s, a penguin Swim()s. You could copy‑paste the animal code into each class, but that's a maintenance nightmare. Instead, you use inheritance: you define a base Animal class, and then Lion, Elephant, and Penguin inherit from it. They automatically get all the animal's members and can add or override their own.

In this lesson, you'll learn what inheritance is, why it exists, how to use it correctly, and when to prefer composition over inheritance.

What Is It?

The Simple Explanation

Inheritance is a mechanism that allows you to define a new class (the derived or child class) based on an existing class (the base or parent class). The derived class automatically receives all the members (fields, methods, properties) of the base class and can add new members or override existing ones.

The Technical Definition

In C#, inheritance is a fundamental object‑oriented principle. A class can inherit from a single base class (single inheritance), but it can implement multiple interfaces. The derived class extends the base class and can:

Inheritance establishes an "is‑a" relationship: a Lion is an Animal. This is also known as subtyping.

Aspect Base Class (Animal) Derived Class (Dog)
MembersName, Age, Eat()Inherits Name, Age, Eat() + adds Bark()
ConstructorAnimal(string name)Dog(string name) : base(name) { }
Virtual methodsvirtual void Speak()override void Speak() { ... }
PolymorphismAnimal a = new Dog();a.Speak() calls Dog's Speak

Why Does It Exist?

The Problem

Without inheritance, you'd have to duplicate common code across many classes. If you needed to add a new property or fix a bug, you'd have to change every class. This leads to code duplication, maintenance nightmares, and inconsistencies. Also, you'd lose the ability to treat different types uniformly (polymorphism).

The Solution

Inheritance solves these problems by:

Big Picture

Inheritance creates a hierarchy. Here's a typical inheritance chain in a .NET application:

INHERITANCE HIERARCHY
Base Class
Product — Id, Name, Price, Description, GetDisplayInfo()
Derived Class
ElectronicProduct — inherits all Product members, adds BatteryLife, Voltage
Further Derived
Laptop — adds ScreenSize, KeyboardType, overrides GetDisplayInfo()
Polymorphism
A method that takes Product can accept any derived type. The correct GetDisplayInfo() is called at runtime.

How It Works

Step 1 — Define a base class

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

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

    public virtual void Speak() => Console.WriteLine($"{Name} makes a sound.");
}

The virtual keyword allows derived classes to override the method.

Step 2 — Derive a class

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

    public override void Speak() => Console.WriteLine($"{Name} barks!");
}

The : Animal syntax declares inheritance. The constructor calls the base constructor with base(name). override replaces the base method.

Step 3 — Polymorphic usage

Animal myAnimal = new Dog("Rex");
myAnimal.Speak(); // Output: "Rex barks!" (calls Dog's Speak)

The actual method called is determined at runtime based on the object's type, not the variable's type.

Step 4 — base keyword

public override void Speak()
{
    base.Speak(); // Call base implementation first
    Console.WriteLine("... and wags its tail!");
}

Use base to access base class members from the derived class.

Step 5 — Abstract classes and members

public abstract class Shape
{
    public abstract double GetArea(); // No implementation
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double GetArea() => Math.PI * Radius * Radius;
}

Abstract classes cannot be instantiated. They provide a template for derived classes.

Simple Example

public class Vehicle
{
    public string Brand { get; set; }
    public int Year { get; set; }

    public Vehicle(string brand, int year) => (Brand, Year) = (brand, year);

    public virtual void Start() => Console.WriteLine("Vehicle starting...");
}

public class Car : Vehicle
{
    public int NumberOfDoors { get; set; }

    public Car(string brand, int year, int doors) : base(brand, year)
    {
        NumberOfDoors = doors;
    }

    public override void Start()
    {
        base.Start();
        Console.WriteLine("Car engine roars to life!");
    }
}

// Usage
Car myCar = new Car("Tesla", 2023, 4);
myCar.Start();
// Output:
// Vehicle starting...
// Car engine roars to life!

Code → Meaning → Result

Real-World Example

Consider an e‑commerce system that needs to handle different types of products. A base Product class provides common properties, and derived classes add specialized behavior.

public abstract class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }

    public virtual string GetDescription() => $"{Name} (${Price:F2})";

    public abstract decimal CalculateTax(); // Each product type has its own tax rule
}

public class ElectronicProduct : Product
{
    public int WarrantyMonths { get; set; }

    public override decimal CalculateTax() => Price * 0.20m; // 20% VAT

    public override string GetDescription() =>
        base.GetDescription() + $", Warranty: {WarrantyMonths} months";
}

public class BookProduct : Product
{
    public string Author { get; set; } = "";

    public override decimal CalculateTax() => Price * 0.05m; // 5% tax on books

    public override string GetDescription() =>
        base.GetDescription() + $", Author: {Author}";
}

// Usage in a service
public class CartService
{
    public decimal CalculateTotalTax(IEnumerable products)
    {
        decimal total = 0;
        foreach (var p in products)
            total += p.CalculateTax(); // Polymorphic call
        return total;
    }
}

// Example
var products = new List
{
    new ElectronicProduct { Id = 1, Name = "Laptop", Price = 1000, WarrantyMonths = 24 },
    new BookProduct { Id = 2, Name = "C# Basics", Price = 50, Author = "John Doe" }
};

var service = new CartService();
Console.WriteLine($"Total tax: {service.CalculateTotalTax(products):C}");
// Output: Total tax: $202.50 (200 + 2.5)

Why this is realistic:

Analogy

Base class = Blueprint for a vehicle

A base class is like a general blueprint for a vehicle — it defines wheels, an engine, and how to start it. You can't build a vehicle from this abstract blueprint (if it's abstract), but it gives the foundation.

Derived class = Specialized blueprint (Car, Truck, Motorcycle)

A derived class is a specialized blueprint that inherits everything from the vehicle blueprint but adds specifics: a car has four doors, a truck has a cargo bed, a motorcycle has two wheels. They are all vehicles, but they behave differently.

Polymorphism = The mechanic works on "any vehicle"

If you have a mechanic who knows how to fix a "vehicle," they can actually fix a car, truck, or motorcycle because each responds to the same "repair" instruction. The mechanic doesn't need to know which specific vehicle until they start working.

Under the Hood

What happens inside the .NET runtime when you use inheritance and polymorphism?

INTERNALS OF INHERITANCE
1. TYPE HIERARCHY

Each class in .NET inherits (directly or indirectly) from System.Object. The runtime maintains a type hierarchy and method tables.

2. VIRTUAL METHOD TABLE (VTable)

For each class, the runtime builds a table of virtual method addresses. When you call a virtual method, the runtime looks up the method in the vtable of the actual object type, not the reference type. This enables polymorphic behavior.

Animal vtable: Speak → Animal.Speak()
Dog vtable:    Speak → Dog.Speak()   (overrides the entry)
3. CONSTRUCTOR CHAINING

When a derived class is instantiated, the base class constructor is called first (implicitly or explicitly with base). This ensures the base part of the object is initialized before the derived part.

4. SEALED CLASSES

The sealed keyword prevents further derivation. The runtime can optimize method calls on sealed classes because the exact type is known at compile time.

5. ABSTRACT METHODS

Abstract methods have no implementation in the base class; they act as placeholders that force derived classes to provide an implementation. The runtime ensures that no object of the abstract class can be created.

Common Confusion

1. Inheritance vs Composition

Inheritance represents an "is‑a" relationship (a Dog is an Animal). Composition represents a "has‑a" relationship (a Car has an Engine). Favor composition over inheritance because it's more flexible and less fragile. Inheritance can lead to deep, brittle hierarchies.

2. Virtual vs Abstract vs Override

3. Base class constructor vs derived constructor

The derived constructor must call the base constructor (implicitly if the base has a parameterless constructor, or explicitly with base(...)). If the base constructor is not called, the base class may not be properly initialized.

Common Mistakes

Mistake 1 — Forgetting to call the base constructor

public class Animal
{
    public Animal(string name) => Name = name;
}
public class Dog : Animal
{
    public Dog(string name) { } //  Compiler error: base constructor not called
}

Correct: public Dog(string name) : base(name) { }

Mistake 2 — Overriding a method without override or new

If you define a method with the same name as a base class method but without override, the compiler warns you that you are hiding the base member. This is usually unintended.

Use override to replace the base method, or new if you intentionally want to hide it (rare).

Mistake 3 — Deep inheritance hierarchies

Too many levels of inheritance make the code hard to understand and maintain. A good rule of thumb is to keep hierarchies shallow (max 3–4 levels). Prefer composition or interfaces to avoid deep chains.

Mistake 4 — Using inheritance for code reuse only

If you are inheriting just to reuse a few methods, and there is no "is‑a" relationship, you should favor composition (e.g., dependency injection, helper classes). Inheritance creates a tight coupling between classes.

When Should I Use It?

Use inheritance when:

Avoid inheritance when:

Mental Model

Base class = general concept (Animal).
Derived class = specific specialization (Dog).
Inheritance = the derived class is a base class.
Virtual/override = the derived class can change behavior.
Polymorphism = using derived objects through base type references.

Remember:
· Inheritance is for "is‑a" relationships.
· Favor composition over inheritance when there is no clear hierarchy.
· Keep hierarchies shallow.
· Use sealed to prevent unintended derivation.

Key Takeaway


Check Your Understanding

You've learned the ins and outs of inheritance. Let's see if you can apply these concepts in practice.

1. Which of the following correctly describes inheritance in C#?

Show answer

Correct: B

Why B is correct: C# supports single inheritance for classes (a class can have only one direct base class), but it can implement multiple interfaces. This is the standard inheritance model in C#.

Why A is incorrect: C# does not support multiple inheritance for classes. You can only inherit from one base class.

Why C is incorrect: Structs are value types and cannot inherit from classes; they can only implement interfaces.

Why D is incorrect: Inheritance works with both abstract and concrete base classes.

Reinforcement: C# uses single inheritance for classes, but multiple interfaces provide flexibility.

2. What keyword do you use in a derived class to call a base class constructor?

Show answer

Correct: B

Why B is correct: The base keyword is used to call a base class constructor (or access base class members) from a derived class.

Why A is incorrect: this refers to the current instance, not the base.

Why C and D are incorrect: super and parent are not C# keywords.

Reinforcement: Use base to call the base constructor.

3. What is the purpose of the virtual keyword in a base class?

Show answer

Correct: B

Why B is correct: The virtual keyword indicates that a derived class can override the method using the override keyword. It provides a default implementation that can be replaced.

Why A is incorrect: sealed prevents overriding, not virtual.

Why C is incorrect: abstract methods have no implementation and must be overridden.

Why D is incorrect: static methods cannot be overridden.

Reinforcement: virtual enables polymorphism.

4. Which of the following is a valid reason to favor composition over inheritance?

Show answer

Correct: B

Why B is correct: Composition (having a class contain instances of other classes) is often more flexible because you can change behavior at runtime, avoid deep hierarchies, and reduce coupling. It's a key principle: "Favor composition over inheritance."

Why A is incorrect: Inheritance is not necessarily faster; it depends on the context. Composition can be just as efficient.

Why C is incorrect: Inheritance and interfaces work together; a class can inherit from a base and implement interfaces.

Why D is incorrect: Composition can sometimes require more code (wiring up dependencies), but it's more maintainable.

Reinforcement: Composition provides better decoupling and flexibility than inheritance in many scenarios.

5. Consider the following code:

class Base { public virtual void M() => Console.Write("Base"); }
class Derived : Base { public override void M() => Console.Write("Derived"); }

Base obj = new Derived();
obj.M();

What is the output?

Show answer

Correct: B

Why B is correct: The variable obj is of type Base, but it references a Derived object. Because M() is virtual and overridden, the runtime calls the derived implementation, printing "Derived".

Why A is incorrect: That would happen if the method were not virtual or if the variable were used statically.

Why C is incorrect: Both methods are not called; only the derived override runs.

Why D is incorrect: The code compiles and runs correctly.

Reinforcement: Polymorphism ensures that the overridden method is called based on the actual object type.

You now have a solid understanding of inheritance — from basic syntax to design principles!


dotnetmadeeasy.com — Learn C# and .NET, the right way.