Skip to main content

Inheritance

Object-Oriented Programming Fundamentals

Before diving into inheritance specifically, it's worth establishing the broader context. Inheritance is one of the four fundamental pillars of Object-Oriented Programming (OOP), alongside:

  1. Encapsulation: Bundling data and methods that operate on that data within a single unit (a class), while hiding internal implementation details
  2. Inheritance: Creating new classes based on existing ones, reusing and extending functionality
  3. Polymorphism: The ability for objects of different types to be treated through a common interface, or for methods to behave differently based on the object calling them
  4. Abstraction: Hiding complex implementation details and exposing only essential features of an object

These principles work together to create maintainable, flexible, and reusable code. Inheritance, in particular, addresses code reuse and establishes hierarchical relationships between classes.

What is Inheritance?

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 gets all the public and protected members of the base class, and can add its own unique members or override existing ones.

Think of it as a taxonomy: a Dog is a type of Animal. A SavingsAccount is a type of BankAccount. The specific type inherits characteristics from the general type while adding its own specializations.

The classic illustration:

        Animal
/ \
Dog Cat
/ \
Beagle Labrador

Each level inherits from the one above, gaining all parent capabilities while potentially adding new ones.

Why Use Inheritance?

Code Reuse: Write common functionality once in the base class, and all derived classes get it automatically.

Logical Hierarchy: Model real-world relationships in code—a SportsCar genuinely is a type of Car.

Polymorphism Support: Derived classes can be treated as instances of their base class, enabling flexible and extensible designs.

Maintenance: Update shared behavior in one place (the base class) rather than duplicating changes across multiple classes.

The "Is-A" Relationship

A key principle: inheritance should represent an "is-a" relationship. A Cat is an Animal. A Manager is an Employee. If you can't naturally say "X is a Y," inheritance probably isn't the right tool.

Contrast this with composition (the "has-a" relationship). A Car has an Engine, but a Car isn't an Engine. In this case, you'd use composition, not inheritance.

Inheritance in .NET/C#

C# supports single inheritance, meaning a class can inherit from only one base class. This is in contrast to languages like C++ that support multiple inheritance. The reasoning is sound: multiple inheritance introduces complexity and ambiguity (the infamous "diamond problem") that outweigh its benefits.

Basic Syntax

The syntax for inheritance in C# uses a colon:

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

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

public class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name} says: Woof!");
}
}

The Dog class inherits from Animal, gaining the Name property and Eat() method automatically. It adds its own Bark() method:

var dog = new Dog { Name = "Buddy" };
dog.Eat(); // Inherited from Animal
dog.Bark(); // Defined in Dog

The base Keyword

The base keyword allows a derived class to access members of its base class, particularly useful in constructors:

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

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

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

public Dog(string name, string breed) : base(name)
{
Breed = breed;
}
}

The : base(name) syntax calls the parent constructor, ensuring the Animal portion of the Dog is properly initialized.

Virtual and Override

By default, methods in C# are not overridable. To allow a derived class to provide its own implementation, mark the base class method as virtual, and use override in the derived class:

public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
}

public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}

public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}

This enables polymorphism—you can treat different derived types uniformly through their base type:

Animal[] animals = { new Dog(), new Cat() };

foreach (var animal in animals)
{
animal.MakeSound(); // Calls the appropriate override
}

Abstract Classes

Sometimes a base class is so general that instantiating it directly doesn't make sense. That's where abstract classes come in:

public abstract class Shape
{
public abstract double CalculateArea();

public void Display()
{
Console.WriteLine($"Area: {CalculateArea()}");
}
}

public class Circle : Shape
{
public double Radius { get; set; }

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

An abstract class:

  • Cannot be instantiated directly
  • May contain abstract methods (no implementation) that derived classes must implement
  • May also contain concrete methods with implementations

Sealed Classes

The opposite of abstract is sealed. A sealed class cannot be inherited from:

public sealed class Configuration
{
// Implementation details
}

// This would cause a compiler error:
// public class ExtendedConfiguration : Configuration { }

Use sealed when you want to prevent further derivation, often for security or performance reasons.

Access Modifiers and Inheritance

Understanding visibility is crucial:

  • public: Accessible everywhere, including derived classes
  • protected: Accessible within the class and derived classes
  • private: Accessible only within the defining class (not inherited)
  • protected internal: Accessible within the same assembly or derived classes
  • private protected: Accessible within the same assembly and only in derived classes

Typically, implementation details are private, while members intended for derived classes are protected.

Inheritance in Practice: DbContext

A practical example from Entity Framework Core demonstrates inheritance effectively. When creating a database context, you inherit from DbContext:

public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}

public DbSet<Platform> Platforms { get; set; }
}

Here, AppDbContext inherits from the DbContext base class, gaining all the database interaction capabilities—connection management, change tracking, query translation, etc.—without implementing any of it yourself. You simply:

  • Call the base constructor with configuration options
  • Define your DbSet properties

This is inheritance at its best: leveraging a rich, battle-tested base class while focusing only on your specific requirements.

When Not to Use Inheritance

Despite its power, inheritance can be overused. Prefer composition over inheritance when:

  • The relationship isn't truly "is-a"
  • You need flexibility to change behavior at runtime
  • The inheritance hierarchy becomes too deep (more than 3-4 levels is a code smell)
  • You're just reusing code without a logical relationship

Deep inheritance hierarchies become brittle and hard to understand. Modern C# provides alternatives like interfaces, composition, and extension methods that often yield cleaner designs.

Interfaces vs. Inheritance

C# compensates for single-inheritance limitations with interfaces. A class can inherit from one base class but implement multiple interfaces:

public class Dog : Animal, IComparable, ICloneable
{
// Implementation
}

Interfaces define contracts (what an object can do) without providing implementation. Use inheritance for "is-a" relationships and shared implementation; use interfaces for "can-do" relationships and capabilities.

The Fragile Base Class Problem

A word of caution: changes to a base class ripple through all derived classes. This coupling can make refactoring risky. Always consider whether modifications to a base class might break derived classes. This is known as the "fragile base class problem," and it's a legitimate concern in deep hierarchies.

Summary

Inheritance is a powerful tool for modeling hierarchical relationships and reusing code. In .NET, it's implemented through single inheritance with support for virtual methods, abstract classes, and sealed classes to control the inheritance chain.

Use it judiciously:

  • Model genuine "is-a" relationships
  • Leverage proven base classes like DbContext, Controller, or Exception
  • Keep hierarchies shallow
  • Favor composition when the relationship isn't truly hierarchical

When used appropriately, inheritance creates elegant, maintainable code. When misused, it creates tangled messes. As with most things in software development, the answer to "should I use inheritance?" is: it depends.


For more information on inheritance in C#, see the official Microsoft documentation on inheritance.