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
- The compiler reads the base class and derived class declarations and records the inheritance relationship in metadata.
- When
new Dog("Rex")runs, the CLR allocates one object large enough for the inheritedAnimalstate and theDogstate. - The base constructor runs first. If the derived constructor has
: base(name), that argument is passed to the selected base constructor. - The derived constructor body runs after the base part is initialized.
- When a non-virtual method is called, the compiler can bind the call to the method known from the variable type.
- When a virtual method is called, the CLR checks the object’s runtime type and dispatches to the most specific override.
- 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
virtualonly when overriding is expected and supported. - Use
abstractclasses when the base concept is incomplete on its own. - Prefer
overrideovernewwhen replacing behavior polymorphically. - Call
baseconstructors to initialize inherited required state. - Keep fields
privateby default. Useprotectedsparingly because it exposes implementation details to subclasses. - Consider interfaces when unrelated classes need to share a capability without sharing code.
- Use
sealedfor classes or overrides that are not designed for further inheritance.
Practice Exercises
- Create an abstract
Notificationclass with an abstractSendmethod. ImplementEmailNotificationandSmsNotification, then store both in aNotification[]. - Create a base
Productclass with a virtualGetPricemethod. Add aDiscountedProductclass that overrides it. - Create a
BankAccountbase class and aSavingsAccountderived 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.
virtualandoverrideenable runtime polymorphism.protectedgives 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.
