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.
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.
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:
new keyword (though this is rarely needed)Inheritance establishes an "is‑a" relationship: a Lion is an Animal. This is also known as subtyping.
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).
Inheritance solves these problems by:
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.
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.
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.
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.
base keywordpublic 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.
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.
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
Car inherits Brand and Year from Vehicle.NumberOfDoors.base(brand, year).Start() overrides the base method, calling the base implementation and adding extra behavior.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:
Product abstract class defines a contract with an abstract method CalculateTax().CartService works with Product references and relies on polymorphism — it doesn't need to know the concrete type.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.
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.
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.
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.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.
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) { }
override or newIf 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).
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.
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.
Car is a Vehicle).sealed to prevent unintended derivation.
virtual, override, and abstract to control how members are overridden.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#?
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?
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?
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?
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?
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.