Skip to main content

3.2 - Inheritance

Inheritance is one of the four pillars of object-oriented programming. It allows a class to inherit properties and behaviors from another class, promoting code reuse and establishing a relationship between a more general class (the base class) and a more specialized class (the derived class).

3.2.1 - Base and Derived Classes

In C#, inheritance is implemented using the : symbol. A class that inherits from another class is called a derived class, and the class it inherits from is called a base class.

// Base class
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }

public Animal(string name, int age)
{
Name = name;
Age = age;
}

public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}

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

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

public override void MakeSound()
{
Console.WriteLine("Woof!");
}

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

In this example:

  • Animal is the base class with properties Name and Age, and a method MakeSound().
  • Dog is a derived class that inherits from Animal. It adds a new property Breed and overrides the MakeSound() method.
  • The Dog class also adds a new method Fetch() that is specific to dogs.

Using Derived Classes

// Creating an instance of the derived class
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");

// Accessing properties from the base class
Console.WriteLine($"Name: {myDog.Name}, Age: {myDog.Age}");

// Accessing property from the derived class
Console.WriteLine($"Breed: {myDog.Breed}");

// Calling overridden method
myDog.MakeSound(); // Output: Woof!

// Calling method specific to the derived class
myDog.Fetch(); // Output: Buddy is fetching the ball!

3.2.2 - Method Overriding

Method overriding allows a derived class to provide a specific implementation of a method that is already defined in its base class. To override a method, the base class method must be declared with the virtual keyword, and the derived class method must use the override keyword.

public class Shape
{
public virtual double CalculateArea()
{
return 0;
}
}

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

public Circle(double radius)
{
Radius = radius;
}

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

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public Rectangle(double width, double height)
{
Width = width;
Height = height;
}

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

Using Polymorphism with Method Overriding

Shape[] shapes = new Shape[2];
shapes[0] = new Circle(5);
shapes[1] = new Rectangle(4, 6);

foreach (Shape shape in shapes)
{
Console.WriteLine($"Area: {shape.CalculateArea()}");
}
// Output:
// Area: 78.53981633974483
// Area: 24

3.2.3 - Abstract Classes

Abstract classes are classes that cannot be instantiated and are designed to be inherited by other classes. They can contain abstract methods (methods without implementation) that must be implemented by derived classes.

// Abstract class
public abstract class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }

// Constructor
public Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}

// Regular method with implementation
public void DisplayInfo()
{
Console.WriteLine($"{Year} {Make} {Model}");
}

// Abstract method (no implementation)
public abstract double CalculateFuelEfficiency();

// Virtual method (can be overridden)
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
}

// Derived class
public class Car : Vehicle
{
public int FuelTankCapacity { get; set; }
public int MilesPerGallon { get; set; }

public Car(string make, string model, int year, int fuelTankCapacity, int milesPerGallon)
: base(make, model, year)
{
FuelTankCapacity = fuelTankCapacity;
MilesPerGallon = milesPerGallon;
}

// Implementing the abstract method
public override double CalculateFuelEfficiency()
{
return MilesPerGallon;
}

// Overriding the virtual method
public override void StartEngine()
{
Console.WriteLine("Car engine started with a vroom!");
}
}

Using Abstract Classes

// Cannot instantiate an abstract class
// Vehicle vehicle = new Vehicle("Toyota", "Corolla", 2020); // Error

// Can instantiate a derived class
Car myCar = new Car("Toyota", "Corolla", 2020, 12, 30);
myCar.DisplayInfo(); // Output: 2020 Toyota Corolla
Console.WriteLine($"Fuel Efficiency: {myCar.CalculateFuelEfficiency()} MPG"); // Output: Fuel Efficiency: 30 MPG
myCar.StartEngine(); // Output: Car engine started with a vroom!

3.2.4 - Sealed Classes

Sealed classes are classes that cannot be inherited. They are used to prevent further derivation of a class.

// Sealed class
public sealed class FinalClass
{
public void DoSomething()
{
Console.WriteLine("Doing something in the sealed class");
}
}

// Cannot inherit from a sealed class
// public class DerivedFromFinal : FinalClass // Error
// {
// }

You can also seal individual methods or properties to prevent them from being overridden in derived classes.

public class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("BaseClass.Method1");
}

public virtual void Method2()
{
Console.WriteLine("BaseClass.Method2");
}
}

public class DerivedClass : BaseClass
{
// Sealed override - cannot be overridden further
public sealed override void Method1()
{
Console.WriteLine("DerivedClass.Method1");
}

// Regular override - can be overridden in further derived classes
public override void Method2()
{
Console.WriteLine("DerivedClass.Method2");
}
}

public class FurtherDerivedClass : DerivedClass
{
// Cannot override Method1 because it's sealed
// public override void Method1() // Error
// {
// }

// Can override Method2
public override void Method2()
{
Console.WriteLine("FurtherDerivedClass.Method2");
}
}

3.2.5 - Inheritance Hierarchies

Inheritance hierarchies represent the relationships between classes in a tree-like structure, with more general classes at the top and more specialized classes at the bottom.

// Base class
public class Person
{
public string Name { get; set; }
public int Age { get; set; }

public Person(string name, int age)
{
Name = name;
Age = age;
}

public virtual void Introduce()
{
Console.WriteLine($"Hi, I'm {Name} and I'm {Age} years old.");
}
}

// First level of derived classes
public class Student : Person
{
public string StudentId { get; set; }
public string Major { get; set; }

public Student(string name, int age, string studentId, string major)
: base(name, age)
{
StudentId = studentId;
Major = major;
}

public override void Introduce()
{
Console.WriteLine($"Hi, I'm {Name}, a {Major} student with ID {StudentId}.");
}

public void Study()
{
Console.WriteLine($"{Name} is studying {Major}.");
}
}

public class Employee : Person
{
public string EmployeeId { get; set; }
public string Department { get; set; }

public Employee(string name, int age, string employeeId, string department)
: base(name, age)
{
EmployeeId = employeeId;
Department = department;
}

public override void Introduce()
{
Console.WriteLine($"Hi, I'm {Name}, an employee in the {Department} department.");
}

public virtual void Work()
{
Console.WriteLine($"{Name} is working in the {Department} department.");
}
}

// Second level of derived classes
public class GraduateStudent : Student
{
public string ThesisTopic { get; set; }

public GraduateStudent(string name, int age, string studentId, string major, string thesisTopic)
: base(name, age, studentId, major)
{
ThesisTopic = thesisTopic;
}

public override void Introduce()
{
Console.WriteLine($"Hi, I'm {Name}, a graduate student researching {ThesisTopic}.");
}

public void Research()
{
Console.WriteLine($"{Name} is researching {ThesisTopic}.");
}
}

public class Manager : Employee
{
public List<Employee> Team { get; set; }

public Manager(string name, int age, string employeeId, string department)
: base(name, age, employeeId, department)
{
Team = new List<Employee>();
}

public override void Introduce()
{
Console.WriteLine($"Hi, I'm {Name}, the manager of the {Department} department.");
}

public override void Work()
{
Console.WriteLine($"{Name} is managing the {Department} department.");
}

public void AddTeamMember(Employee employee)
{
Team.Add(employee);
Console.WriteLine($"{employee.Name} added to {Name}'s team.");
}
}

Using Inheritance Hierarchies

// Creating instances of different classes in the hierarchy
Person person = new Person("Alice", 30);
Student student = new Student("Bob", 20, "S12345", "Computer Science");
Employee employee = new Employee("Charlie", 35, "E67890", "Engineering");
GraduateStudent gradStudent = new GraduateStudent("Dave", 25, "G54321", "Physics", "Quantum Computing");
Manager manager = new Manager("Eve", 40, "M13579", "Marketing");

// Calling methods
person.Introduce();
student.Introduce();
student.Study();
employee.Introduce();
employee.Work();
gradStudent.Introduce();
gradStudent.Study();
gradStudent.Research();
manager.Introduce();
manager.Work();
manager.AddTeamMember(employee);

3.2.6 - The 'base' Keyword

The base keyword is used to access members of the base class from within a derived class. It is commonly used in constructors to call the base class constructor and in overridden methods to call the base class implementation.

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

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

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

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

// Using base to call the base class constructor
public Dog(string name, string breed) : base(name)
{
Breed = breed;
}

// Using base to call the base class method
public override void Eat()
{
base.Eat(); // Call the base class implementation
Console.WriteLine($"{Name} the {Breed} is eating dog food.");
}

public void Bark()
{
Console.WriteLine($"{Name} the {Breed} is barking.");
}
}

Using the 'base' Keyword

Dog myDog = new Dog("Rex", "German Shepherd");
myDog.Eat();
// Output:
// Rex is eating.
// Rex the German Shepherd is eating dog food.

3.2.7 - Constructor Chaining

Constructor chaining is the process of calling one constructor from another constructor in the same class or from a derived class to a base class constructor.

Constructor Chaining Within the Same Class

public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }

// Primary constructor
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}

// Constructor that chains to the primary constructor
public Person(string firstName, string lastName) : this(firstName, lastName, 0)
{
// Additional initialization if needed
}

// Another constructor that chains to the primary constructor
public Person() : this("Unknown", "Unknown", 0)
{
// Additional initialization if needed
}
}

Constructor Chaining Between Base and Derived Classes

public class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }

public Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}

public Vehicle(string make, string model) : this(make, model, DateTime.Now.Year)
{
}
}

public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public string FuelType { get; set; }

// Chaining to a base class constructor
public Car(string make, string model, int year, int numberOfDoors, string fuelType)
: base(make, model, year)
{
NumberOfDoors = numberOfDoors;
FuelType = fuelType;
}

// Chaining to another constructor in the same class
public Car(string make, string model, int year)
: this(make, model, year, 4, "Gasoline")
{
}

// Chaining to a base class constructor with fewer parameters
public Car(string make, string model)
: base(make, model)
{
NumberOfDoors = 4;
FuelType = "Gasoline";
}
}

3.2.8 - Virtual Methods

Virtual methods are methods that can be overridden in derived classes. They allow for polymorphic behavior, where the method called is determined by the actual type of the object at runtime.

public class Shape
{
// Virtual method
public virtual double CalculateArea()
{
return 0;
}

// Non-virtual method
public void DisplayType()
{
Console.WriteLine($"This is a {GetType().Name}");
}
}

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

public Circle(double radius)
{
Radius = radius;
}

// Override of virtual method
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public Rectangle(double width, double height)
{
Width = width;
Height = height;
}

// Override of virtual method
public override double CalculateArea()
{
return Width * Height;
}
}

Using Virtual Methods

// Creating objects
Shape shape = new Shape();
Circle circle = new Circle(5);
Rectangle rectangle = new Rectangle(4, 6);

// Calling methods directly
Console.WriteLine($"Shape area: {shape.CalculateArea()}");
Console.WriteLine($"Circle area: {circle.CalculateArea()}");
Console.WriteLine($"Rectangle area: {rectangle.CalculateArea()}");

// Polymorphism with virtual methods
Shape[] shapes = new Shape[] { shape, circle, rectangle };
foreach (Shape s in shapes)
{
Console.WriteLine($"{s.GetType().Name} area: {s.CalculateArea()}");
s.DisplayType();
}

3.2.9 - Hiding Base Class Members

In addition to overriding virtual methods, C# allows hiding base class members using the new keyword. This is different from overriding because it doesn't use polymorphism; the method called depends on the reference type, not the object type.

public class BaseClass
{
public void Method()
{
Console.WriteLine("BaseClass.Method");
}
}

public class DerivedClass : BaseClass
{
// Hiding the base class method
public new void Method()
{
Console.WriteLine("DerivedClass.Method");
}
}

Using Hidden Members

BaseClass baseObj = new BaseClass();
DerivedClass derivedObj = new DerivedClass();
BaseClass baseRefToDerived = derivedObj;

baseObj.Method(); // Output: BaseClass.Method
derivedObj.Method(); // Output: DerivedClass.Method
baseRefToDerived.Method(); // Output: BaseClass.Method (not polymorphic)

3.2.10 - Inheritance Best Practices

When using inheritance in C#, consider the following best practices:

  1. Use Inheritance for "is-a" Relationships: Inheritance should model an "is-a" relationship. For example, a Dog is an Animal, so it makes sense for Dog to inherit from Animal.

  2. Favor Composition Over Inheritance: When the relationship is more of a "has-a" nature, use composition instead of inheritance. For example, a Car has an Engine, so it's better to have an Engine property in the Car class rather than having Car inherit from Engine.

  3. Keep Inheritance Hierarchies Shallow: Deep inheritance hierarchies can be difficult to understand and maintain. Try to keep them no more than 2-3 levels deep.

  4. Design for Inheritance or Prohibit It: Classes should be designed with inheritance in mind, or they should be sealed to prevent inheritance. Document how derived classes should interact with base class methods.

  5. Use Abstract Classes for Common Base Functionality: When multiple classes share common functionality, consider using an abstract base class to encapsulate that functionality.

  6. Prefer Interfaces for Multiple Inheritance: C# doesn't support multiple inheritance of classes, but it does support implementing multiple interfaces. Use interfaces when a class needs to inherit behavior from multiple sources.

  7. Override Object Methods When Appropriate: Consider overriding Equals(), GetHashCode(), and ToString() when creating classes that will be used in collections or when string representation is important.

  8. Be Careful with Constructor Chaining: Ensure that constructors properly initialize all necessary state, and be aware of the order of initialization when chaining constructors.

  9. Use Virtual Methods Judiciously: Only make methods virtual if they are intended to be overridden. Virtual method calls are slightly slower than non-virtual calls.

  10. Avoid Hiding Members When Possible: Prefer overriding virtual members to hiding members with the new keyword, as hiding can lead to confusion and bugs.

Example of Good Inheritance Design

// Abstract base class with common functionality
public abstract class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }

protected Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}

// Common method with default implementation
public virtual void DisplayInfo()
{
Console.WriteLine($"{Year} {Make} {Model}");
}

// Abstract method that derived classes must implement
public abstract void Move();

// Override of Object.ToString()
public override string ToString()
{
return $"{Year} {Make} {Model}";
}
}

// Derived class for cars
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public string FuelType { get; set; }

public Car(string make, string model, int year, int numberOfDoors, string fuelType)
: base(make, model, year)
{
NumberOfDoors = numberOfDoors;
FuelType = fuelType;
}

// Implementation of abstract method
public override void Move()
{
Console.WriteLine($"The {Make} {Model} is driving on the road.");
}

// Override of virtual method
public override void DisplayInfo()
{
base.DisplayInfo();
Console.WriteLine($"Doors: {NumberOfDoors}, Fuel: {FuelType}");
}
}

// Another derived class
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }

public Bicycle(string make, string model, int year, int numberOfGears)
: base(make, model, year)
{
NumberOfGears = numberOfGears;
}

// Implementation of abstract method
public override void Move()
{
Console.WriteLine($"The {Make} {Model} is being pedaled.");
}

// Override of virtual method
public override void DisplayInfo()
{
base.DisplayInfo();
Console.WriteLine($"Gears: {NumberOfGears}");
}
}

// Composition example
public class Engine
{
public int Horsepower { get; set; }
public int Cylinders { get; set; }

public Engine(int horsepower, int cylinders)
{
Horsepower = horsepower;
Cylinders = cylinders;
}

public void Start()
{
Console.WriteLine($"Engine with {Horsepower} HP and {Cylinders} cylinders started.");
}
}

// Car with composition
public class CarWithEngine : Vehicle
{
// Composition - Car has an Engine
public Engine Engine { get; set; }

public CarWithEngine(string make, string model, int year, Engine engine)
: base(make, model, year)
{
Engine = engine;
}

public override void Move()
{
Engine.Start();
Console.WriteLine($"The {Make} {Model} is driving with its {Engine.Horsepower} HP engine.");
}
}

Using Well-Designed Inheritance

// Creating objects
Car car = new Car("Toyota", "Corolla", 2020, 4, "Gasoline");
Bicycle bicycle = new Bicycle("Trek", "Mountain", 2019, 21);
Engine v8Engine = new Engine(300, 8);
CarWithEngine carWithEngine = new CarWithEngine("Ford", "Mustang", 2021, v8Engine);

// Using polymorphism
Vehicle[] vehicles = new Vehicle[] { car, bicycle, carWithEngine };
foreach (Vehicle vehicle in vehicles)
{
vehicle.DisplayInfo();
vehicle.Move();
Console.WriteLine();
}

In the next section, we'll explore polymorphism, another key principle of object-oriented programming that allows objects of different classes to be treated as objects of a common base class.