C# Inheritance

Inheritance lets one C# class build on another class instead of starting from nothing. A derived class receives the accessible members of a base class, then adds or changes behavior for a more specific type. It matters because it enables reuse, polymorphism, and shared contracts, but it must be used carefully so classes stay clear and maintainable.

Overview: How Inheritance Works

In C#, inheritance creates an is-a relationship. A Dog can inherit from Animal because a dog is an animal. A Manager can inherit from Employee because a manager is an employee. This is different from a has-a relationship: a car has an engine, so an Engine field is usually better than making Car inherit from Engine.

A derived class automatically includes the instance fields and methods of its base class, subject to access rules. public members are usable by any caller that can access the object. protected members are usable inside the base class and inside derived classes. private members still exist inside the object, but derived class code cannot access them directly; it must use public or protected members supplied by the base class.

C# supports single class inheritance: a class can inherit from only one base class. However, every class also inherits, directly or indirectly, from object. That is why all C# objects have methods such as ToString, Equals, and GetHashCode. A class may also implement multiple interfaces, which is often the better choice when you want a type to promise behavior without sharing implementation.

Inheritance becomes especially powerful with polymorphism. If a base class declares a method as virtual, a derived class can provide a more specific implementation with override. Code can store derived objects in base-class variables, call the virtual method, and the CLR dispatches the call to the most specific override for the actual runtime object. This is called dynamic dispatch.

Under the hood, a derived object contains the base portion and the derived portion as one object on the managed heap. Base constructors run before derived constructors so the inherited state is initialized first. The runtime type information for the object records its real type, so a variable declared as Animal can still refer to an actual Dog object and call Dog‘s override.

Syntax

public class Employee
{
    public string Name { get; }

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

    public virtual decimal CalculatePay()
    {
        return 0m;
    }
}

public class SalariedEmployee : Employee
{
    public decimal Salary { get; }

    public SalariedEmployee(string name, decimal salary) : base(name)
    {
        Salary = salary;
    }

    public override decimal CalculatePay()
    {
        return Salary;
    }
}
Part Meaning
: Employee Makes SalariedEmployee inherit from Employee.
base(name) Calls a constructor in the base class before the derived constructor body runs.
virtual Allows a derived class to replace the method implementation.
override Provides the replacement implementation for a virtual or abstract base member.
protected Allows derived classes to use a member while keeping it hidden from unrelated code.
sealed Prevents further inheritance, or prevents a specific override from being overridden again.

Examples

Example 1: A Basic Base Class and Derived Class

using System;

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

    public string Name { get; }

    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} says woof.");
    }

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

class Program
{
    static void Main()
    {
        Dog dog = new Dog("Rex");
        Console.WriteLine($"{dog.Name} is a dog.");
        dog.Speak();
        dog.Fetch();
    }
}

Output:

Rex is a dog.
Rex says woof.
Rex fetches the ball.

Dog inherits the Name property from Animal. Its constructor calls base(name) because the base class is responsible for initializing that property. Speak is virtual in the base class and overridden in the derived class, so a dog can speak in its own way while still sharing the common animal state.

Example 2: Polymorphism With an Abstract Base Class

using System;
using System.Globalization;

abstract class Shape
{
    public Shape(string name)
    {
        Name = name;
    }

    public string Name { get; }

    public abstract double Area();
}

class Circle : Shape
{
    public Circle(double radius) : base("Circle")
    {
        Radius = radius;
    }

    public double Radius { get; }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : Shape
{
    public Rectangle(double width, double height) : base("Rectangle")
    {
        Width = width;
        Height = height;
    }

    public double Width { get; }
    public double Height { get; }

    public override double Area()
    {
        return Width * Height;
    }
}

class Program
{
    static void Main()
    {
        Shape[] shapes = { new Circle(2), new Rectangle(3, 4) };

        foreach (Shape shape in shapes)
        {
            string area = shape.Area().ToString("0.00", CultureInfo.InvariantCulture);
            Console.WriteLine($"{shape.Name}: {area}");
        }
    }
}

Output:

Circle: 12.57
Rectangle: 12.00

Shape is abstract, so it cannot be instantiated directly. It says that every shape must have an Area method, but each derived class decides how the area is calculated. The array is declared as Shape[], yet each call to Area uses the runtime type: Circle or Rectangle.

Example 3: Protected State and Calling the Base Implementation

using System;

class Vehicle
{
    protected int speed;

    public int Speed => speed;

    public virtual void Accelerate(int amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        speed += amount;
    }
}

class ElectricScooter : Vehicle
{
    private const int MaxSpeed = 25;

    public override void Accelerate(int amount)
    {
        base.Accelerate(amount);

        if (speed > MaxSpeed)
        {
            speed = MaxSpeed;
        }
    }
}

class Program
{
    static void Main()
    {
        ElectricScooter scooter = new ElectricScooter();
        scooter.Accelerate(10);
        scooter.Accelerate(30);

        Console.WriteLine(scooter.Speed);
    }
}

Output:

25

The base class owns the general acceleration rule: negative acceleration is invalid. The derived scooter class reuses that rule by calling base.Accelerate(amount), then adds a scooter-specific speed cap. The speed field is protected, so derived classes can read and assign it while ordinary callers must use the public Speed property.

How It Works Step by Step

  1. The compiler reads the base class and derived class declarations and records the inheritance relationship in metadata.
  2. When new Dog("Rex") runs, the CLR allocates one object large enough for the inherited Animal state and the Dog state.
  3. The base constructor runs first. If the derived constructor has : base(name), that argument is passed to the selected base constructor.
  4. The derived constructor body runs after the base part is initialized.
  5. When a non-virtual method is called, the compiler can bind the call to the method known from the variable type.
  6. When a virtual method is called, the CLR checks the object’s runtime type and dispatches to the most specific override.
  7. If no derived override exists, the inherited base implementation runs.

This dispatch behavior is why inheritance is useful for extensible code. A report generator can work with Shape or Employee references without knowing every concrete subtype. The tradeoff is coupling: a derived class depends on base-class design decisions, so changing a base class can affect every derived class.

Common Mistakes

Mistake 1: Hiding a Method Instead of Overriding It

using System;

class Worker
{
    public virtual void PrintRole()
    {
        Console.WriteLine("Worker");
    }
}

class Intern : Worker
{
    public new void PrintRole()
    {
        Console.WriteLine("Intern");
    }
}

class Program
{
    static void Main()
    {
        Worker worker = new Intern();
        worker.PrintRole();
    }
}

Output:

Worker

The new keyword hides the base method; it does not participate in polymorphic dispatch. Because the variable is typed as Worker, the hidden method is not called. Use override when the derived type is supposed to replace virtual behavior.

using System;

class Worker
{
    public virtual void PrintRole()
    {
        Console.WriteLine("Worker");
    }
}

class Intern : Worker
{
    public override void PrintRole()
    {
        Console.WriteLine("Intern");
    }
}

class Program
{
    static void Main()
    {
        Worker worker = new Intern();
        worker.PrintRole();
    }
}

Output:

Intern

Mistake 2: Trying to Access Private Base Fields

class Account
{
    private decimal balance;
}

class SavingsAccount : Account
{
    public void AddInterest()
    {
        balance += 5m;
    }
}

This does not compile because private means accessible only inside the declaring class. A derived class does not get direct access to private fields. Expose a protected method, a protected property, or a public operation that preserves the base class’s rules.

using System;

class Account
{
    public decimal Balance { get; private set; }

    protected void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        Balance += amount;
    }
}

class SavingsAccount : Account
{
    public void AddInterest()
    {
        Deposit(5m);
    }
}

class Program
{
    static void Main()
    {
        SavingsAccount account = new SavingsAccount();
        account.AddInterest();
        Console.WriteLine(account.Balance);
    }
}

Output:

5

Best Practices

  • Use inheritance only for true is-a relationships. Use fields, properties, and services for has-a relationships.
  • Keep base classes small and stable. Every protected member becomes part of the contract for derived classes.
  • Mark methods virtual only when overriding is expected and supported.
  • Use abstract classes when the base concept is incomplete on its own.
  • Prefer override over new when replacing behavior polymorphically.
  • Call base constructors to initialize inherited required state.
  • Keep fields private by default. Use protected sparingly because it exposes implementation details to subclasses.
  • Consider interfaces when unrelated classes need to share a capability without sharing code.
  • Use sealed for classes or overrides that are not designed for further inheritance.

Practice Exercises

  1. Create an abstract Notification class with an abstract Send method. Implement EmailNotification and SmsNotification, then store both in a Notification[].
  2. Create a base Product class with a virtual GetPrice method. Add a DiscountedProduct class that overrides it.
  3. Create a BankAccount base class and a SavingsAccount derived class that adds interest through a protected deposit helper.

Summary

  • Inheritance lets a derived class reuse and specialize a base class.
  • C# classes have single class inheritance, but all classes ultimately inherit from object.
  • Base constructors run before derived constructors.
  • virtual and override enable runtime polymorphism.
  • protected gives derived classes access while hiding members from unrelated callers.
  • Use inheritance for clear is-a relationships, and prefer composition or interfaces when sharing a capability is enough.