C# Inheritance for Beginners: Base and Derived Classes
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:
- Inherit all non‑private members (fields, properties, methods, events, etc.)
- Override virtual and abstract members to provide specialized behavior
- Add new members that are not present in the base class
- Hide base class members using the
newkeyword (though this is rarely needed)
Inheritance establishes an "is‑a" relationship: a Lion is an Animal. This is also known as subtyping.
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:
- Code reuse — common logic lives in one place (the base class).
- Extensibility — you can add or override behavior in derived classes.
- Polymorphism — you can write code that works with the base class but actually uses derived class implementations at runtime.
- Modeling real‑world relationships — many domains have hierarchical classifications (e.g., Vehicle → Car → SportsCar).
Big Picture
Inheritance creates a hierarchy. Here's a typical inheritance chain in a .NET application:
Product — Id, Name, Price, Description, GetDisplayInfo()
ElectronicProduct — inherits all Product members, adds BatteryLife, Voltage
Laptop — adds ScreenSize, KeyboardType, overrides GetDisplayInfo()
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
CarinheritsBrandandYearfromVehicle.- It adds its own property
NumberOfDoors. - The constructor chains to the base constructor using
base(brand, year). Start()overrides the base method, calling the base implementation and adding extra behavior.
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:
- The
Productabstract class defines a contract with an abstract methodCalculateTax(). - Each derived class provides its own tax calculation.
- The
CartServiceworks withProductreferences and relies on polymorphism — it doesn't need to know the concrete type.
Analogy
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.
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.
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?
Each class in .NET inherits (directly or indirectly) from System.Object. The runtime maintains a type hierarchy and method tables.
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)
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.
The sealed keyword prevents further derivation. The runtime can optimize method calls on sealed classes because the exact type is known at compile time.
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
virtual— a member that can be overridden but has a default implementation.abstract— a member with no implementation; must be overridden in a derived class. Can only exist in abstract classes.override— provides a new implementation for a virtual/abstract member from the base class.
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:
- There is a clear "is‑a" relationship (e.g.,
Caris aVehicle). - You need to share common code across multiple classes.
- You want to enable polymorphism — treat derived objects as base objects.
- You are designing a framework or library where base classes provide a template for extensibility.
Avoid inheritance when:
- The relationship is "has‑a" (composition is more appropriate).
- You only need to reuse a few methods — consider composition or extension methods.
- You expect the hierarchy to change frequently — inheritance makes changes ripple through the hierarchy.
- You are working with value types (structs) — they cannot inherit.
Mental Model
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
- Inheritance allows a class to reuse and extend the functionality of an existing class.
- It establishes an "is‑a" relationship and enables polymorphism.
- Use
virtual,override, andabstractto control how members are overridden. - Base constructors must be called from derived constructors.
- Favor composition over inheritance when the relationship is not clearly "is‑a".
- Keep inheritance hierarchies shallow to avoid complexity.
- In .NET 10 / C# 14, inheritance remains a foundational pillar of object‑oriented design.
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!