Easy Learn C#

Inheritance in C#

Introduction to Inheritance

Inheritance is a fundamental concept in Object-Oriented Programming that allows a class to inherit properties and behaviors from another class. It promotes code reuse and establishes an "is-a" relationship between classes.

Why Use Inheritance?

  • Reuse code from existing classes
  • Create specialized versions of classes
  • Build hierarchical class relationships
  • Implement common behavior in a base class
  • Extend functionality without modifying existing code

Basic Inheritance Syntax

In C#, inheritance is implemented using the colon (:) syntax. A class that inherits from another is called a derived class (or subclass), while the class being inherited from is called a base class (or superclass).

Creating a Base and Derived Class


// Base class
public class Animal
{
    // Fields
    public string Name { get; set; }
    public int Age { get; set; }
    
    // Constructor
    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }
    
    // Methods
    public void Eat()
    {
        Console.WriteLine($"{Name} is eating.");
    }
    
    public void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping.");
    }
    
    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} makes a sound.");
    }
}

// Derived class inheriting from Animal
public class Dog : Animal
{
    // Additional property specific to Dog
    public string Breed { get; set; }
    
    // Constructor that calls the base class constructor
    public Dog(string name, int age, string breed) : base(name, age)
    {
        Breed = breed;
    }
    
    // Override the MakeSound method from the base class
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} barks loudly!");
    }
    
    // Additional method specific to Dog
    public void Fetch()
    {
        Console.WriteLine($"{Name} fetches the ball.");
    }
}

// Usage
Dog dog = new Dog("Rex", 3, "German Shepherd");
dog.Eat();        // Inherited from Animal
dog.Sleep();      // Inherited from Animal
dog.MakeSound();  // Overridden in Dog
dog.Fetch();      // Defined in Dog
Console.WriteLine($"{dog.Name} is a {dog.Breed}.");  // Accessing properties

Key concepts in this example:

  • Dog inherits all public and protected members from Animal
  • The derived class constructor calls the base class constructor using the base keyword
  • The virtual keyword allows methods to be overridden in derived classes
  • The override keyword indicates a method that replaces a virtual method from the base class
  • The derived class can define additional members specific to it

Inheritance Types

C# supports single inheritance for classes (one direct base class) but can implement multiple interfaces. This approach prevents the "diamond problem" found in languages with multiple inheritance.

Single Inheritance


public class A { /* ... */ }
public class B : A { /* ... */ }     // B inherits from A
public class C : B { /* ... */ }     // C inherits from B (and indirectly from A)

Multilevel Inheritance


public class Vehicle
{
    public void Start() { Console.WriteLine("Vehicle started"); }
}

public class Car : Vehicle
{
    public void Drive() { Console.WriteLine("Car is driving"); }
}

public class SportsCar : Car
{
    public void Accelerate() { Console.WriteLine("SportsCar accelerating quickly"); }
}

// Usage
SportsCar ferrari = new SportsCar();
ferrari.Start();      // From Vehicle
ferrari.Drive();      // From Car
ferrari.Accelerate(); // From SportsCar

Hierarchical Inheritance


public class Animal
{
    public void Breathe() { Console.WriteLine("Breathing"); }
}

public class Mammal : Animal
{
    public void FeedMilk() { Console.WriteLine("Feeding milk"); }
}

public class Bird : Animal
{
    public void Fly() { Console.WriteLine("Flying"); }
}

public class Fish : Animal
{
    public void Swim() { Console.WriteLine("Swimming"); }
}

// All three classes inherit from Animal
Mammal dog = new Mammal();
Bird eagle = new Bird();
Fish shark = new Fish();

dog.Breathe();   // From Animal
eagle.Breathe(); // From Animal
shark.Breathe(); // From Animal

Method Overriding and the Virtual Keyword

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

Virtual and Override Methods


public class Shape
{
    public string Name { get; set; }
    
    // Virtual method that can be overridden
    public virtual void Draw()
    {
        Console.WriteLine($"Drawing a {Name}");
    }
    
    // Virtual method that calculates area
    public virtual double CalculateArea()
    {
        return 0;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public Circle(double radius)
    {
        Name = "Circle";
        Radius = radius;
    }
    
    // Override the Draw method
    public override void Draw()
    {
        // Call the base implementation first
        base.Draw();
        Console.WriteLine($"with radius {Radius}");
    }
    
    // Override the CalculateArea 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)
    {
        Name = "Rectangle";
        Width = width;
        Height = height;
    }
    
    // Override the Draw method
    public override void Draw()
    {
        // Completely replace the base implementation
        Console.WriteLine($"Drawing a {Name} with width {Width} and height {Height}");
    }
    
    // Override the CalculateArea method
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Usage
Shape[] shapes = { new Circle(5), new Rectangle(4, 6) };

foreach (var shape in shapes)
{
    shape.Draw();
    Console.WriteLine($"Area: {shape.CalculateArea()}");
    Console.WriteLine();
}

/* Output:
Drawing a Circle
with radius 5
Area: 78.53981633974483

Drawing a Rectangle with width 4 and height 6
Area: 24
*/

Key points about virtual and override methods:

  • The virtual keyword indicates that a method can be overridden in derived classes
  • The override keyword indicates that a method is replacing a virtual method from the base class
  • The base keyword can be used to call the base class implementation from the overriding method
  • Overridden methods can completely replace or extend the base functionality
  • Method overriding enables polymorphic behavior (different implementations of the same method)

Access Modifiers and Inheritance

Access modifiers control which members of a base class are accessible to derived classes.

Member Accessibility in Inheritance

Access Modifier Accessible from Derived Class?
public Yes - accessible from anywhere
protected Yes - accessible only in the derived class, not from outside
internal Yes - if in the same assembly
protected internal Yes - from derived classes or within the same assembly
private No - only accessible in the declaring class
private protected Yes - but only if the derived class is in the same assembly

public class Base
{
    public int PublicField = 1;        // Accessible from anywhere
    protected int ProtectedField = 2;  // Accessible from derived classes
    private int PrivateField = 3;      // Not accessible from derived classes
    internal int InternalField = 4;    // Accessible within the same assembly
    
    protected internal int ProtectedInternalField = 5;  // Accessible from derived classes or within the same assembly
    private protected int PrivateProtectedField = 6;    // Accessible from derived classes within the same assembly
}

public class Derived : Base
{
    public void AccessBaseMembers()
    {
        Console.WriteLine(PublicField);             // OK
        Console.WriteLine(ProtectedField);          // OK
        // Console.WriteLine(PrivateField);         // Error - not accessible
        Console.WriteLine(InternalField);           // OK if in the same assembly
        Console.WriteLine(ProtectedInternalField);  // OK
        Console.WriteLine(PrivateProtectedField);   // OK if in the same assembly
    }
}

The base Keyword

The base keyword is used to access members of the base class from within a derived class.

Using the base Keyword


public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
    
    public virtual void Introduce()
    {
        Console.WriteLine($"Hi, I'm {FirstName} {LastName}.");
    }
}

public class Student : Person
{
    public int StudentId { get; set; }
    
    // Calling the base class constructor
    public Student(string firstName, string lastName, int studentId) 
        : base(firstName, lastName)
    {
        StudentId = studentId;
    }
    
    // Extending the base class method
    public override void Introduce()
    {
        // Call the base class implementation first
        base.Introduce();
        
        // Add additional behavior
        Console.WriteLine($"My student ID is {StudentId}.");
    }
    
    // Method to show full details
    public void ShowDetails()
    {
        Console.WriteLine($"Name: {base.FirstName} {base.LastName}");
        Console.WriteLine($"ID: {StudentId}");
    }
}

// Usage
Student student = new Student("John", "Doe", 12345);
student.Introduce();
/* Output:
Hi, I'm John Doe.
My student ID is 12345.
*/

Uses of the base keyword:

  • Call the base class constructor using base(parameters) in the derived class constructor
  • Call a base class method from an overriding method using base.MethodName()
  • Access base class members using base.MemberName
  • Clarify which implementation to use when names are the same in base and derived classes

Preventing Inheritance and Overriding

C# provides keywords to prevent inheritance and method overriding when needed.

The sealed Keyword


// Sealed class - cannot be inherited from
public sealed class FinalClass
{
    public void DoSomething()
    {
        Console.WriteLine("This method is in a sealed class");
    }
}

// Error: Cannot derive from sealed type 'FinalClass'
// public class AttemptedDerivedClass : FinalClass
// {
// }

// Regular class with a sealed method
public class BaseWithSealedMethod
{
    public virtual void RegularMethod()
    {
        Console.WriteLine("This method can be overridden");
    }
    
    public virtual void AnotherMethod()
    {
        Console.WriteLine("This method can be overridden");
    }
}

public class DerivedWithSealedMethod : BaseWithSealedMethod
{
    // Regular override
    public override void RegularMethod()
    {
        Console.WriteLine("This method overrides the base method");
    }
    
    // Sealed override - cannot be further overridden in derived classes
    public sealed override void AnotherMethod()
    {
        Console.WriteLine("This method overrides the base method and is sealed");
    }
}

public class FurtherDerived : DerivedWithSealedMethod
{
    // This is allowed
    public override void RegularMethod()
    {
        Console.WriteLine("This method overrides again");
    }
    
    // Error: Cannot override sealed method
    // public override void AnotherMethod()
    // {
    // }
}

Ways to restrict inheritance:

  • Use sealed on a class to prevent any classes from inheriting from it
  • Use sealed override on a method to allow overriding in the current class but prevent further overriding in derived classes
  • Make a class static to prevent instantiation and inheritance
  • Use private constructors to prevent external inheritance (but allow nested classes to inherit)

Abstract Classes and Methods

Abstract classes cannot be instantiated and may contain abstract methods that derived classes must implement.

Abstract Class Example


// Abstract class - cannot be instantiated directly
public abstract class Shape
{
    public string Color { get; set; }
    
    // Constructor in an abstract class
    public Shape(string color)
    {
        Color = color;
    }
    
    // Regular method - implementation provided
    public void SetColor(string color)
    {
        Color = color;
        Console.WriteLine($"Shape color set to {color}");
    }
    
    // Abstract method - no implementation, must be overridden
    public abstract double CalculateArea();
    
    // Abstract method - no implementation, must be overridden
    public abstract void Draw();
    
    // Virtual method - has implementation but can be overridden
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"This is a {GetType().Name} with color {Color}");
    }
}

// Concrete class implementing the abstract class
public class Circle : Shape
{
    public double Radius { get; set; }
    
    public Circle(double radius, string color) : base(color)
    {
        Radius = radius;
    }
    
    // Implementation of abstract method
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    
    // Implementation of abstract method
    public override void Draw()
    {
        Console.WriteLine($"Drawing a {Color} circle with radius {Radius}");
    }
}

// Another concrete class implementing the abstract class
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public Rectangle(double width, double height, string color) : base(color)
    {
        Width = width;
        Height = height;
    }
    
    // Implementation of abstract method
    public override double CalculateArea()
    {
        return Width * Height;
    }
    
    // Implementation of abstract method
    public override void Draw()
    {
        Console.WriteLine($"Drawing a {Color} rectangle with width {Width} and height {Height}");
    }
    
    // Override virtual method
    public override void DisplayInfo()
    {
        base.DisplayInfo();
        Console.WriteLine($"Width: {Width}, Height: {Height}");
    }
}

// Usage
// Shape shape = new Shape("Red");  // Error: Cannot create an instance of an abstract class

Shape circle = new Circle(5, "Red");
Shape rectangle = new Rectangle(4, 6, "Blue");

circle.Draw();
rectangle.Draw();

Console.WriteLine($"Circle area: {circle.CalculateArea()}");
Console.WriteLine($"Rectangle area: {rectangle.CalculateArea()}");

circle.DisplayInfo();
rectangle.DisplayInfo();

/* Output:
Drawing a Red circle with radius 5
Drawing a Blue rectangle with width 4 and height 6
Circle area: 78.53981633974483
Rectangle area: 24
This is a Circle with color Red
This is a Rectangle with color Blue
Width: 4, Height: 6
*/

Key characteristics of abstract classes:

  • Cannot be instantiated directly
  • Can contain both abstract methods (no implementation) and concrete methods (with implementation)
  • Can have constructors, fields, properties, and non-abstract methods
  • Must be inherited by a derived class to be used
  • Derived classes must implement all abstract methods
  • Useful for defining a common base with some implementation and some required behaviors

Inheritance Best Practices

Guidelines for Effective Inheritance

  • Use inheritance for "is-a" relationships - A Car "is-a" Vehicle, not a Car "has-a" Vehicle
  • Keep inheritance hierarchies shallow - Deep hierarchies can be confusing and hard to maintain
  • Follow the Liskov Substitution Principle - Derived classes should be substitutable for their base class
  • Design for inheritance or prohibit it - Either make a class properly inheritable or seal it
  • Prefer composition over inheritance when appropriate - "has-a" relationships are better modeled with composition
  • Document for inheritors - Document the intended way to extend your class
  • Consider interfaces - Sometimes interfaces provide a more flexible solution than inheritance
  • Use abstract base classes for common implementation with required behaviors
  • Ensure that base class invariants are preserved in derived classes

Common Inheritance Mistakes to Avoid

  • Breaking encapsulation - Exposing private implementation details to derived classes
  • Using inheritance for code reuse only without a proper "is-a" relationship
  • Creating fragile base classes - Changes in base classes can break derived classes
  • Not considering the implications of overrides on the base class behavior
  • Creating overly deep inheritance hierarchies - More than 2-3 levels can become unwieldy
  • Using "protected" too freely - Exposing internal state through protected members
  • Not validating inputs in overridden methods - Always ensure method contracts are maintained