ASP.NET Core

Class Inheritance in C#

Class inheritance in C# - fundamentals, implementation, and best practices for building hierarchical class structures.

https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance

Implement base and derived classes

What is Class Inheritance?

Class inheritance is one of the primary characteristics of object-oriented programming. It allows you to create a hierarchy of classes by defining relationships between classes. In C#, you can create a base class that defines common behavior and characteristics, and then create derived classes that inherit and extend that behavior.

For example, a base class named Person might define Name and Age properties. A derived class named Employee might inherit these properties and define other properties such as EmployeeNumber and Salary.

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

public class Employee : Person
{
    public int EmployeeNumber { get; set; }
    public decimal Salary { get; set; }
}

Notice the colon (:) character in the Employee class declaration. The colon between Employee and Person indicates that the Employee class inherits from the Person class. When an Employee object is created, it has access to the Name and Age properties defined in the Person class, as well as the EmployeeNumber and Salary properties defined in the Employee class. When a Person object is created, it only has access to the Name and Age properties.

When you use inheritance in C#, the system:

  1. Creates a parent-child relationship between classes
  2. Enables derived classes to reuse base class members
  3. Allows derived classes to extend functionality with new members
  4. Supports polymorphic behavior through method overriding
  5. Provides a structured approach to code organization
  6. Facilitates maintainable and scalable application design

Class inheritance ensures modularity at scale by enabling you to build upon existing functionality rather than rewriting code, creating a foundation for complex software architectures.


Why Use Class Inheritance?

1. Code Reuse

Inheritance allows you to reuse code defined in a base class in a derived class. This reduces duplication and promotes code reuse across your application.

2. Extensibility

Inheritance enables you to extend the behavior of a base class by adding new members to a derived class. You can define new properties, methods, and events in a derived class without modifying the base class.

3. Encapsulation

Inheritance promotes encapsulation by allowing you to hide the implementation details of a base class from a derived class. Inheritance and encapsulation enable you to define a clear interface for interacting with objects of a derived class.

4. Consistency

Inheritance promotes consistency by allowing you to define common behavior in a base class. Inheriting from a base class ensures that the derived classes share the same base behaviors.

5. Polymorphism

Inheritance enables polymorphism, which allows you to treat objects of a derived class as objects of their base class. Polymorphism enables you to write code that works with objects of different types without knowing their specific type at compile time.

6. Logical Organization

Create hierarchical structures that mirror real-world relationships, making your codebase more intuitive and easier to understand.


How Class Inheritance Works

Basic Concept

Class inheritance in C# creates an "is-a" relationship where a derived class inherits members from a base class and can extend or modify that behavior:

Inheritance Flow:

Base Class Definition
    ↓
Derived Class Declaration (: BaseClass)
    ↓
├─ Inherits public/protected members
├─ Can add new members
├─ Can override virtual members
└─ Can hide base members with 'new'

Example - Animal Hierarchy:

// Base class
public class Animal
{
    public string Name { get; set; }
    
    public virtual void MakeSound()
    {
        Console.WriteLine("Some generic animal sound");
    }
}

// Derived class
public class Dog : Animal
{
    public string Breed { get; set; }
    
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

// Usage
Dog myDog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
myDog.MakeSound(); // Output: Woof!
Console.WriteLine(myDog.Name); // Output: Buddy (inherited property)

Important: C# supports single inheritance only (a class can inherit from one base class), but a class can implement multiple interfaces.


Class Inheritance vs Interface Implementation

C# provides two mechanisms for defining relationships between classes: class inheritance and interface implementation. Both mechanisms enable code reuse and promote polymorphism, but they have different characteristics.

Class Inheritance Characteristics

  • A class can inherit from only one base class (single inheritance)
  • A derived class can reuse, extend, and modify the behavior defined in the base class
  • Inheritance creates an "is-a" relationship between classes
  • Provides implementation inheritance (both interface and implementation are inherited)

Interface Implementation Characteristics

  • A class can implement multiple interfaces
  • A class can define its own behavior and implement the members defined in an interface
  • Interface implementation creates a "can-do" relationship between classes
  • Provides only interface inheritance (no implementation is inherited)

"Is-A" vs "Can-Do" Relationships

The difference between "is-a" and "can-do" relationships is important when designing object-oriented systems:

Use class inheritance when: A derived class is a specialized version of a base class.

Example - "Is-A" Relationship:

public class Dog
{
    public string Name { get; set; }
    public virtual void Bark()
    {
        Console.WriteLine("Woof!");
    }
}

public class GermanShepherd : Dog
{
    public string CoatColor { get; set; }
    
    public override void Bark()
    {
        Console.WriteLine("Loud Woof!");
    }
}

public class GoldenRetriever : Dog
{
    public bool IsServiceDog { get; set; }
}

In this example, GermanShepherd and GoldenRetriever are specialized versions of Dog. They inherit common properties and methods from the Dog class and can extend or modify that behavior.

Use interface implementation when: A class can perform a specific set of actions.

Example - "Can-Do" Relationship:

public interface IDrawable
{
    void Draw();
}

public class Circle : IDrawable
{
    public double Radius { get; set; }
    
    public void Draw()
    {
        Console.WriteLine($"Drawing a circle with radius {Radius}");
    }
}

public class Rectangle : IDrawable
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public void Draw()
    {
        Console.WriteLine($"Drawing a rectangle {Width}x{Height}");
    }
}

In this example, Circle and Rectangle can be drawn. They implement the IDrawable interface to define a common set of actions shared by different classes, even though they are not related through inheritance.


Class Inheritance and Polymorphism

Class inheritance and polymorphism are closely related concepts in object-oriented programming. Inheritance allows you to define a hierarchy of classes that share common behaviors, while polymorphism allows you to treat objects of a derived class as objects of their base class.

Polymorphism Through Inheritance

Consider a base class named HousePet and derived classes named Dog and Cat. The HousePet class defines a Speak method that returns a string representing the sound the pet makes. The Dog class overrides the Speak method to return "Woof", and the Cat class overrides the Speak method to return "Meow".

public class HousePet
{
    public virtual string Speak()
    {
        return "Hello";
    }
}

public class Dog : HousePet
{
    public override string Speak()
    {
        return "Woof";
    }
}

public class Cat : HousePet
{
    public override string Speak()
    {
        return "Meow";
    }
}

You can create a HousePet object that references a Dog or Cat instance and call the Speak method to get the appropriate response:

HousePet myPet1 = new Dog();    // Create a HousePet object that's of type Dog
HousePet myPet2 = new Cat();    // Create a HousePet object that's of type Cat

Console.WriteLine(myPet1.Speak());    // Output: Woof
Console.WriteLine(myPet2.Speak());    // Output: Meow

In this example, the HousePet class defines a common Speak behavior that the Dog and Cat classes share. The Dog and Cat classes override the Speak method to customize the behavior. When you call the Speak method on a HousePet object that references one of the derived classes, you get the appropriate response based on the actual type of the object.

Polymorphism allows you to write code that works with objects of different types without knowing their specific type at compile time. In C#, polymorphism can be achieved by using either class inheritance or interface implementation.


Core Concepts

Base and Derived Classes

A base class (also called parent or superclass) provides the foundation, while a derived class (also called child or subclass) extends or specializes that foundation.

Structure:

// Base class
public class Vehicle
{
    public string Brand { get; set; }
    public int Year { get; set; }
    
    public void Start()
    {
        Console.WriteLine($"{Brand} is starting...");
    }
    
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"Brand: {Brand}, Year: {Year}");
    }
}

// Derived class
public class Car : Vehicle
{
    public int NumberOfDoors { get; set; }
    
    public override void DisplayInfo()
    {
        base.DisplayInfo(); // Call base class method
        Console.WriteLine($"Doors: {NumberOfDoors}");
    }
    
    public void Honk()
    {
        Console.WriteLine("Beep beep!");
    }
}

Access Modifiers in Inheritance

Access modifiers control the visibility of base class members in derived classes:

Member Inheritance Rules:

When a class inherits from a base class, the following rules apply:

  • Static and instance constructors are not inherited
  • All other members of the base class are inherited, but access modifiers affect their visibility in the derived class

1. public

  • Accessible from any code that has access to the class
  • Derived classes inherit public members and they're accessible from outside the class hierarchy
  • Visible to external code

Example:

public class BaseClass
{
    public int publicField;
    public void PublicMethod() { }
}

public class DerivedClass : BaseClass
{
    public void AccessPublicMember()
    {
        publicField = 10;
        PublicMethod();
    }
}

In this example, the DerivedClass inherits the publicField and PublicMethod members from the BaseClass. The AccessPublicMember method in the DerivedClass can access these members. Public members are also accessible from code outside the class hierarchy.

2. protected

  • Accessible within the class and derived classes
  • Not accessible from outside the class hierarchy
  • Ideal for members meant for inheritance

Example:

public class BaseClass
{
    protected int protectedField;
    protected void ProtectedMethod() { }
}

public class DerivedClass : BaseClass
{
    public void AccessProtectedMember()
    {
        protectedField = 10;
        ProtectedMethod();
    }
}

In this example, the DerivedClass inherits the protectedField and ProtectedMethod members from the BaseClass. The AccessProtectedMember method in the DerivedClass can access these members. However, if you try to access protected members from outside the class hierarchy, a compile-time error is generated.

3. internal

  • Accessible within the same assembly
  • Not accessible from outside the assembly, even if the class is inherited
  • Inherited if derived class is in same assembly

Example:

public class BaseClass
{
    internal int internalField;
    internal void InternalMethod() { }
}

public class DerivedClass : BaseClass
{
    public void AccessInternalMember()
    {
        internalField = 10;
        InternalMethod();
    }
}

In this example, the DerivedClass inherits the internalField and InternalMethod members from the BaseClass. The AccessInternalMember method in the DerivedClass can access these members because they're in the same assembly. However, if you try to access internal members from outside the assembly, a compile-time error is generated.

4. private

  • Accessible only within the defining class
  • Not inherited - derived classes cannot access private members
  • Encapsulates implementation details

Example:

public class BaseClass
{
    private int privateField;
    private void PrivateMethod() { }
}

public class DerivedClass : BaseClass
{
    public void AccessPrivateMember()
    {
        // Cannot access privateField or PrivateMethod
        // Compile-time error
    }
}

In this example, the DerivedClass inherits from the BaseClass, but it cannot access the privateField or PrivateMethod members because they're private. Private members are not directly accessible in derived classes.

5. protected internal

  • Accessible within same assembly OR in derived classes
  • Combines protected and internal access

6. private protected

  • Accessible within same assembly AND in derived classes
  • Most restrictive combination

Inheritance and Constructors

Constructors are not inherited, but derived classes can call base class constructors:

public class Employee
{
    public string Name { get; set; }
    public int EmployeeId { get; set; }
    
    public Employee(string name, int employeeId)
    {
        Name = name;
        EmployeeId = employeeId;
    }
}

public class Manager : Employee
{
    public int TeamSize { get; set; }
    
    // Constructor must call base constructor
    public Manager(string name, int employeeId, int teamSize) 
        : base(name, employeeId)
    {
        TeamSize = teamSize;
    }
}

// Usage
Manager manager = new Manager("Alice Johnson", 1001, 5);

Constructor Execution Order:

1. Base class constructor executes
2. Derived class constructor executes

Method Overriding and Hiding

Virtual and Override

Use virtual in the base class and override in the derived class to implement polymorphic behavior:

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

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Polymorphic usage
Shape shape1 = new Circle { Radius = 5 };
Shape shape2 = new Rectangle { Width = 4, Height = 6 };

Console.WriteLine(shape1.CalculateArea()); // Uses Circle's implementation
Console.WriteLine(shape2.CalculateArea()); // Uses Rectangle's implementation

Extending a Derived Class with New Members

A derived class is used to extend or modify the base class that it inherits from. When you create a derived class, you have several options for how to extend the base class:

  1. Define new properties and methods that don't exist in the base class - This extends the behavior by providing more functionality
  2. Define properties and methods with the same name as base class members - Use the new keyword to hide base class members
  3. Override properties and methods in the base class - Use the override keyword to extend or modify base class behavior

Adding New Members That Don't Exist in the Base Class

When you create a derived class, you can add new properties and methods that extend the behavior and functionality of the base class without any naming conflicts.

Example:

// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();

// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
baseClass.Method1();

// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
derivedClass.Method1();
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method2();

// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
baseClassReferencingDerivedClass.Method1();
// baseClassReferencingDerivedClass.Method2(); // Error: 'BaseClass' doesn't contain 'Method2'

/*Output:
Base - Property1
Base - Method1

Base - Property1
Base - Method1
Derived - Property2
Derived - Method2

Base - Property1
Base - Method1
*/

public class BaseClass
{
    public string Property1 { get; set; } = "Base - Property1";

    public void Method1()
    {
        Console.WriteLine("Base - Method1");
    }
}

public class DerivedClass : BaseClass
{
    public string Property2 { get; set; } = "Derived - Property2";

    public void Method2()
    {
        Console.WriteLine("Derived - Method2");
    }
}

In this example, the DerivedClass extends the base class with a new property Property2 and a new method Method2. There are no naming conflicts between the base and derived classes, so you can access both new and inherited members from an instance of the derived class.

Key Observations:

  • baseClass can only access members defined in BaseClass
  • derivedClass can access members from both BaseClass and DerivedClass
  • baseClassReferencingDerivedClass is declared as BaseClass but references a DerivedClass instance
  • Even though baseClassReferencingDerivedClass contains a DerivedClass instance, it's treated like a BaseClass and can only access base class members
  • This demonstrates polymorphism - the object's compile-time type determines what members are accessible

Adding New Members with Same Name as Base Class Members

When you add new members to a derived class, you need to be aware of naming conflicts. A naming conflict occurs when a derived class defines a member with the same name as a member in the base class.

Example - Naming Conflict:

// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();

// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();

// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();

// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();

/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2

Base - Property1
Derived - Property2
Base - Method1
Derived - Method2

Base - Property1
Base - Property2
Base - Method1
Base - Method2
*/

public class BaseClass
{
    public string Property1 { get; set; } = "Base - Property1";
    public string Property2 { get; set; } = "Base - Property2";

    public void Method1()
    {
        Console.WriteLine("Base - Method1");
    }

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

public class DerivedClass : BaseClass
{
    public string Property2 { get; set; } = "Derived - Property2";

    public void Method2()
    {
        Console.WriteLine("Derived - Method2");
    }
}

Notice that Property2 and Method2 in the derived class have the same name as members in the base class. This creates a naming conflict and generates compiler warnings:

warning CS0108: 'DerivedClass.Property2' hides inherited member 'BaseClass.Property2'. 
Use the new keyword if hiding was intended.

warning CS0108: 'DerivedClass.Method2()' hides inherited member 'BaseClass.Method2()'. 
Use the new keyword if hiding was intended.

Output Analysis:

  • When using derivedClass (declared as DerivedClass): The derived class versions of Property2 and Method2 are called
  • When using baseClassReferencingDerivedClass (declared as BaseClass): The base class versions of Property2 and Method2 are called
  • The compile-time type determines which member is accessed, not the runtime type

Method Hiding with 'new'

When you have a naming conflict between the base class and the derived class, you can use the new keyword to hide the base class member intentionally. The new keyword allows you to define a new member in the derived class that has the same name as a member in the base class.

Using the 'new' Keyword

Using the new keyword doesn't change the compiler's default behavior for resolving naming conflicts, but it does suppress the warning that occurs when you build the solution, and it makes the intention to hide the base class member explicit.

Example:

// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();

// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();

// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();

// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();

/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2

Base - Property1
Derived - Property2
Base - Method1
Derived - Method2

Base - Property1
Base - Property2
Base - Method1
Base - Method2
*/

public class BaseClass
{
    public string Property1 { get; set; } = "Base - Property1";
    public string Property2 { get; set; } = "Base - Property2";

    public void Method1()
    {
        Console.WriteLine("Base - Method1");
    }

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

public class DerivedClass : BaseClass
{
    public new string Property2 { get; set; } = "Derived - Property2";

    public new void Method2()
    {
        Console.WriteLine("Derived - Method2");
    }
}

When you run this code, the output is the same as the previous example. The new keyword is used in the derived class to hide the Property2 property and the Method2 method defined in the base class.

Behavior:

  • The Property2 property and Method2 method of the derived class are called when using an object declared as DerivedClass
  • The Property2 property and Method2 method of the base class are called when using an object declared as BaseClass (even if it references a DerivedClass instance)
  • No compiler warnings are generated because the intention to hide is explicit

General Guidance on Using the 'new' Keyword

The new keyword can be used to hide a member of the base class intentionally. This behavior is useful when the derived class needs to provide a different implementation or functionality that isn't compatible with the base class member.

Guidelines for using the 'new' keyword:

Use the new keyword when:

  • You want to hide a member of the base class intentionally - Hiding ensures the derived class member is treated as a separate entity
  • You want to avoid accidental overriding of base class members - Using new makes your intention explicit
  • The derived class needs completely different functionality that isn't an extension of the base class behavior
  • You want to make it clear to other developers that hiding is intentional

Avoid using the new keyword when:

  • You intend to extend or modify the behavior of a base class member - Use the override keyword instead
  • It leads to confusion or ambiguity in the code - Ensure the purpose is clear and justified
  • The base class member is not virtual and you're trying to achieve polymorphism - This won't work as new breaks polymorphism

Override vs New:

  • override: Replaces base implementation, enables polymorphism - the derived class method is called even through base class references
  • new: Hides base implementation, breaks polymorphism - which method is called depends on the compile-time type, not runtime type

Example Demonstrating the Difference

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

public class DerivedClass : BaseClass
{
    // Override - enables polymorphism
    public override void VirtualMethod()
    {
        Console.WriteLine("DerivedClass VirtualMethod");
    }
    
    // New - hides base member
    public new void NonVirtualMethod()
    {
        Console.WriteLine("DerivedClass NonVirtualMethod");
    }
}

// Usage
BaseClass obj = new DerivedClass();
obj.VirtualMethod();      // Output: DerivedClass VirtualMethod (polymorphism works)
obj.NonVirtualMethod();   // Output: BaseClass NonVirtualMethod (hiding - no polymorphism)

DerivedClass obj2 = new DerivedClass();
obj2.VirtualMethod();     // Output: DerivedClass VirtualMethod
obj2.NonVirtualMethod();  // Output: DerivedClass NonVirtualMethod

Abstract Classes and Members

Abstract classes cannot be instantiated and are designed to be inherited. They can contain abstract members that must be implemented by derived classes.

The Abstract Keyword

The abstract keyword in C# is used to define classes and class members that are incomplete and must be implemented in derived classes. An abstract class cannot be instantiated directly and is intended to be a base class for other classes. Abstract methods and properties are declared without any implementation and must be overridden in nonabstract derived classes.

Defining Abstract Classes

Example - Shape Hierarchy:

public abstract class Shape
{
    public abstract int GetArea();
}

public class Square : Shape
{
    private int _side;

    public Square(int side)
    {
        _side = side;
    }

    public override int GetArea()
    {
        return _side * _side;
    }
}

class Program
{
    static void Main()
    {
        Square square = new Square(5);
        Console.WriteLine($"Area of the square = {square.GetArea()}");
    }
}

In this example, the Shape class is abstract and contains an abstract method GetArea. The Square class inherits from Shape and provides an implementation for the GetArea method. The Square class can be instantiated, and the GetArea method returns the area of the square.

Abstract Class Rules:

  • Abstract classes: Cannot be instantiated directly. The abstract class is a base class for derived classes, and the derived classes must provide implementations for all abstract members of the abstract class
  • Abstract methods: Declared without any implementation in the abstract class. Derived classes must override these methods and provide the implementation
  • Abstract properties: Similar to abstract methods, abstract properties are declared without implementation and must be overridden in derived classes
  • Can contain concrete members: Abstract classes can have both abstract and concrete (implemented) members

More Complex Example:

public abstract class DatabaseConnection
{
    public string ConnectionString { get; set; }
    
    // Abstract method - no implementation
    public abstract void Connect();
    
    // Abstract property
    public abstract bool IsConnected { get; }
    
    // Concrete method
    public void LogConnection()
    {
        Console.WriteLine($"Connecting to: {ConnectionString}");
    }
}

public class SqlConnection : DatabaseConnection
{
    private bool _isConnected;
    
    public override void Connect()
    {
        Console.WriteLine("Connecting to SQL Server...");
        _isConnected = true;
    }
    
    public override bool IsConnected => _isConnected;
}

public class MongoConnection : DatabaseConnection
{
    private bool _isConnected;
    
    public override void Connect()
    {
        Console.WriteLine("Connecting to MongoDB...");
        _isConnected = true;
    }
    
    public override bool IsConnected => _isConnected;
}

The abstract keyword in C# is a powerful tool for defining incomplete classes and members that must be implemented in derived classes. It enforces a contract that derived classes must follow, ensuring that certain methods and properties are implemented. Appropriate use of the abstract keyword promotes a clear delineation of responsibilities between base and derived classes.


Sealed Classes and Members

The sealed keyword in C# is used to prevent a class or class member from being inherited or overridden. When a class is marked as sealed, it cannot be used as a base class for other classes. When a method is marked as sealed, it cannot be overridden in derived classes.

Sealed Classes

public sealed class FinalImplementation : BaseClass
{
    // This class cannot be inherited
}

// Compilation error
// public class CannotDeriveThis : FinalImplementation { }

Sealed Methods

The sealed keyword is particularly useful when you want to prevent further overriding in an inheritance chain:

Example - Controlling Override Chain:

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

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

public class DerivedClass : BaseClass
{
    public sealed override void Method1()
    {
        Console.WriteLine("Method1 in DerivedClass");
    }

    public override void Method2()
    {
        Console.WriteLine("Method2 in DerivedClass");
    }
}

public class FinalClass : DerivedClass
{
    // This class cannot override Method1 because it's sealed in DerivedClass
    
    public override void Method2()
    {
        Console.WriteLine("Method2 in FinalClass");
    }
}

In this example, the DerivedClass inherits from the BaseClass and overrides the Method1 method, marking it as sealed. The Method2 method is also overridden in the DerivedClass but isn't sealed. The FinalClass inherits from DerivedClass and can override the Method2 method. However, it cannot override the Method1 method because it is sealed in the DerivedClass.

Sealed Keyword Rules:

  • Sealed classes: Cannot be used as a base class for other classes. Prevents inheritance from the sealed class
  • Sealed methods: Cannot be overridden in derived classes. Prevents further modification of the method in derived classes
  • Sealed properties: Similar to sealed methods, cannot be overridden in derived classes
  • Must override first: You can only use sealed on members that are already overriding a virtual or abstract member

Use Cases for Sealed:

  • Prevent unintended inheritance
  • Performance optimization (compiler can make certain optimizations)
  • Security (prevent malicious overriding)
  • Design enforcement (ensure implementation is final)

Sealed classes and methods are useful when you want to prevent further extension or modification of a class or method. They provide a way to restrict inheritance and ensure that certain members remain unchanged.


Overriding Properties and Methods in a Derived Class

In C#, you can use the override keyword to extend or modify the behavior of the base class in the derived class. The override keyword enables you to override properties and methods that are inherited from the base class, and provide custom implementations in the derived class. This allows you to reuse code defined in the base class and extend or modify the behavior in the derived class.

To override a property or method in a derived class, follow these steps:

  1. Declare the members in the base class as either abstract or virtual
  2. Override the members in the derived class using the override keyword
  3. Optionally, use the base keyword to access the base class implementation from the overridden member

Declaring Virtual and Abstract Members in the Base Class

Before you can override members in a derived class, you must declare the members in the base class as either abstract or virtual:

  • abstract: Indicates that the member has no implementation and must be overridden in a derived class
  • virtual: Indicates that the member has an implementation but can be overridden or extended in a derived class

Example:

public class BaseClass
{
    public string Property1 { get; set; } = "Base - Property1";
    public string Property2 { get; set; } = "Base - Property2";

    public virtual void Method1()
    {
        Console.WriteLine("Base - Method1");
    }

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

In this example, Property1 and Property2 are not marked as virtual, so they cannot be overridden. Method1 is marked as virtual, so it can be overridden in derived classes. Method2 is not marked as virtual, so it can only be hidden using the new keyword.

Overriding Members in the Derived Class

After declaring members in the base class as either abstract or virtual, you can override them in the derived class using the override keyword. The override keyword indicates that the member in the derived class replaces the member in the base class.

Example - Override vs New:

// Step 1: Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();

// Step 2: Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();

// Step 3: Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();

// Step 4: Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();

/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2

Derived - Property1
Derived - Property2
Derived - Method1
Derived - Method2

Derived - Property1
Base - Property2
Derived - Method1
Base - Method2
*/

public class BaseClass
{
    public virtual string Property1 { get; set; } = "Base - Property1";
    public virtual string Property2 { get; set; } = "Base - Property2";

    public virtual void Method1()
    {
        Console.WriteLine("Base - Method1");
    }

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

public class DerivedClass : BaseClass
{
    public override string Property1 { get; set; } = "Derived - Property1";
    public new string Property2 { get; set; } = "Derived - Property2";

    public override void Method1()
    {
        Console.WriteLine("Derived - Method1");
    }

    public new void Method2()
    {
        Console.WriteLine("Derived - Method2");
    }
}

This code demonstrates how to modify and hide properties and methods in a derived class using the override and new keywords. The base class has two properties and two methods. The derived class overrides Property1 and Method1, and hides Property2 and Method2 using the new keyword.

Breakdown:

Step 1: Creates three instances:

  • baseClass: Instance of BaseClass
  • derivedClass: Instance of DerivedClass
  • baseClassReferencingDerivedClass: BaseClass reference pointing to a DerivedClass object (polymorphism)

Step 2: Accessing baseClass uses all base class implementations

Step 3: Accessing derivedClass uses the derived class implementations for all members

Step 4: Accessing baseClassReferencingDerivedClass demonstrates the critical difference:

  • For overridden members (Property1 and Method1): The derived class implementation is used (polymorphism works)
  • For hidden members (Property2 and Method2): The base class implementation is used (polymorphism doesn't work)

This demonstrates the fundamental difference between method overriding (using override) and method hiding (using new).

Defining an Abstract Base Class

When you define a base class as abstract, you indicate that the class is incomplete and must be implemented by derived classes. Abstract classes can contain abstract properties and methods that must be implemented by derived classes. Abstract base classes cannot be instantiated directly, so they're used as a template for derived classes to implement the abstract members.

Example:

// Create instances of the base class and the derived classes
// Note: We can't create an instance of an abstract class
// BaseClass baseClass = new BaseClass(); // This line would cause a compile-time error
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();

// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();

// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();

/*Output:
Derived - Property1
Derived - Property2
Derived - Method1
Derived - Method2

Derived - Property1
Base - Property2
Derived - Method1
Base - Method2
*/

public abstract class BaseClass
{
    public abstract string Property1 { get; set; }
    public virtual string Property2 { get; set; } = "Base - Property2";

    public abstract void Method1();

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

public class DerivedClass : BaseClass
{
    public override string Property1 { get; set; } = "Derived - Property1";
    public new string Property2 { get; set; } = "Derived - Property2";

    public override void Method1()
    {
        Console.WriteLine("Derived - Method1");
    }

    public new void Method2()
    {
        Console.WriteLine("Derived - Method2");
    }
}

Notice that when the base class is declared using the abstract keyword, the base class cannot be instantiated directly. Instead, you must create instances of the derived classes that implement the abstract members. The derived class must provide implementations for the abstract members defined in the base class. Members that aren't abstract can be overridden or hidden in the derived class.

Overriding Implicitly Inherited Members of the Object Class

All classes in C# implicitly inherit from the Object class. The Object class defines several members that are inherited by all classes, such as ToString, Equals, and GetHashCode. You can override these members in your classes to provide custom implementations.

Example:

// Create instances of the derived classes
DerivedClass1 derivedClass1 = new DerivedClass1();
DerivedClass2 derivedClass2a = new DerivedClass2 { Property1 = "Value1", Property2 = "Value2" };
DerivedClass2 derivedClass2b = new DerivedClass2 { Property1 = "Value1", Property2 = "Value2" };
DerivedClass2 derivedClass2c = new DerivedClass2 { Property1 = "Value3", Property2 = "Value4" };

// Demonstrate Object class methods for DerivedClass1
Console.WriteLine("\nDemonstrating Object class methods for DerivedClass1:");
Console.WriteLine($"ToString: {derivedClass1.ToString()}");
Console.WriteLine($"Equals: {derivedClass1.Equals(new DerivedClass1())}");
Console.WriteLine($"GetHashCode: {derivedClass1.GetHashCode()}");

// Demonstrate overridden Object class methods for DerivedClass2
Console.WriteLine("\nDemonstrating overridden Object class methods for DerivedClass2:");
Console.WriteLine($"ToString: {derivedClass2a.ToString()}");
Console.WriteLine($"Equals (derivedClass2a vs derivedClass2b): {derivedClass2a.Equals(derivedClass2b)}"); // True
Console.WriteLine($"Equals (derivedClass2a vs derivedClass2c): {derivedClass2a.Equals(derivedClass2c)}"); // False
Console.WriteLine($"GetHashCode (derivedClass2a): {derivedClass2a.GetHashCode()}");
Console.WriteLine($"GetHashCode (derivedClass2b): {derivedClass2b.GetHashCode()}");
Console.WriteLine($"GetHashCode (derivedClass2c): {derivedClass2c.GetHashCode()}");

/*Output:
Demonstrating Object class methods for DerivedClass1:
ToString: DerivedClass1
Equals: False
GetHashCode: [HashCode]

Demonstrating overridden Object class methods for DerivedClass2:
ToString: DerivedClass2: Property1 = Value1, Property2 = Value2
Equals (derivedClass2a vs derivedClass2b): True
Equals (derivedClass2a vs derivedClass2c): False
GetHashCode (derivedClass2a): [HashCode]
GetHashCode (derivedClass2b): [HashCode]
GetHashCode (derivedClass2c): [HashCode]
*/

public abstract class BaseClass
{
    public abstract string Property1 { get; set; }
    public virtual string Property2 { get; set; } = "Base - Property2";

    public abstract void Method1();

    public void Method2()
    {
        Console.WriteLine("Base - Method2");
    }
}

public class DerivedClass1 : BaseClass
{
    public override string Property1 { get; set; } = "Derived1 - Property1";
    public new string Property2 { get; set; } = "Derived1 - Property2";

    public override void Method1()
    {
        Console.WriteLine("Derived1 - Method1");
    }

    public new void Method2()
    {
        Console.WriteLine("Derived1 - Method2");
    }
}

public class DerivedClass2 : BaseClass
{
    public override string Property1 { get; set; } = "Derived2 - Property1";
    public new string Property2 { get; set; } = "Derived2 - Property2";

    public override void Method1()
    {
        Console.WriteLine("Derived2 - Method1");
    }

    public new void Method2()
    {
        Console.WriteLine("Derived2 - Method2");
    }

    // Override ToString method
    public override string ToString()
    {
        return $"DerivedClass2: Property1 = {Property1}, Property2 = {Property2}";
    }

    // Override Equals method
    public override bool Equals(object obj)
    {
        if (obj is DerivedClass2 other)
        {
            return Property1 == other.Property1 && Property2 == other.Property2;
        }
        return false;
    }

    // Override GetHashCode method
    public override int GetHashCode()
    {
        return HashCode.Combine(Property1, Property2);
    }
}

In this example, the DerivedClass1 and DerivedClass2 classes inherit from the BaseClass class. The DerivedClass2 class overrides the ToString, Equals, and GetHashCode methods inherited from the Object class:

  • ToString: Returns a string representation of the object with property values
  • Equals: Compares two objects for equality based on their property values (value equality instead of reference equality)
  • GetHashCode: Returns a hash code for the object based on its property values

The DerivedClass1 class does not override these methods, so it uses the default implementations from the Object class:

  • Default ToString returns the type name
  • Default Equals compares object references (not values)
  • Default GetHashCode returns a hash code based on the object reference

This example demonstrates how overriding Object class methods allows you to customize the behavior of fundamental operations on your objects.


The 'base' Keyword

The base keyword allows access to base class members from within a derived class.

Calling Base Methods

public class Logger
{
    public virtual void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

public class FileLogger : Logger
{
    public string FilePath { get; set; }
    
    public override void Log(string message)
    {
        base.Log(message); // Call base implementation
        File.AppendAllText(FilePath, $"{DateTime.Now}: {message}\n");
    }
}

Calling Base Constructors

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

public class Student : Person
{
    public string StudentId { get; set; }
    
    public Student(string firstName, string lastName, string studentId) 
        : base(firstName, lastName)
    {
        StudentId = studentId;
    }
}

Accessing Base Properties

public class Product
{
    public decimal Price { get; set; }
}

public class DiscountedProduct : Product
{
    public decimal DiscountPercentage { get; set; }
    
    public decimal FinalPrice
    {
        get { return base.Price * (1 - DiscountPercentage / 100); }
    }
}

Implicit Inheritance from Object

In C#, all classes implicitly inherit from the Object class (also known as System.Object). The Object class defines several methods that are available to all classes. If a class doesn't explicitly inherit from another class, it still inherits from Object by default.

Methods Inherited from Object

1. ToString() The ToString method returns a string that represents the current object. By default, it returns the fully qualified name of the class (including namespace).

2. Equals() The Equals method compares two objects for equality. By default, it compares the references of the objects (reference equality).

3. GetHashCode() The GetHashCode method returns a hash code for the current object. By default, it returns the hash code of the object's reference.

Example - Object Methods

Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 };
Person person3 = person1;

Console.WriteLine(person1.ToString());        // Output: Person (or Namespace.Person)
Console.WriteLine(person1.Equals(person2));   // Output: False
Console.WriteLine(person1.GetHashCode());     // Output: 32854180 (example)
Console.WriteLine(person1.Equals(person3));   // Output: True

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

public class Employee : Person
{
    public int EmployeeNumber { get; set; }
    public decimal Salary { get; set; }
}

Output Explanation:

  • ToString() returns the fully qualified name of the Person class (including the namespace if defined)
  • The first Equals() compares person1 and person2 references and returns False because they are different object instances, even though they have the same property values
  • GetHashCode() returns the hash code of the person1 object's reference
  • The second Equals() compares person1 and person3 references and returns True because person3 is a reference to the same object as person1

Overriding Object Methods

You can override these methods in your classes to provide custom behavior:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    // Override ToString
    public override string ToString()
    {
        return $"{Name}, Age: {Age}";
    }
    
    // Override Equals for value equality
    public override bool Equals(object? obj)
    {
        if (obj == null || GetType() != obj.GetType())
            return false;
            
        Person other = (Person)obj;
        return Name == other.Name && Age == other.Age;
    }
    
    // Override GetHashCode (must override when overriding Equals)
    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
}

// Now with overridden methods:
Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 };

Console.WriteLine(person1.ToString());        // Output: Alice, Age: 30
Console.WriteLine(person1.Equals(person2));   // Output: True (value equality)

Best Practices for Object Methods:

  • Always override GetHashCode() when you override Equals()
  • Override ToString() to provide meaningful string representations
  • Consider implementing IEquatable<T> for type-safe equality comparisons
  • Use base.ToString(), base.Equals(), or base.GetHashCode() when appropriate in derived classes

Inheritance Hierarchies

Complex applications often require multiple levels of inheritance.

Multi-Level Inheritance

// Level 1
public class LivingThing
{
    public bool IsAlive { get; set; } = true;
    
    public virtual void Breathe()
    {
        Console.WriteLine("Breathing...");
    }
}

// Level 2
public class Animal : LivingThing
{
    public virtual void Move()
    {
        Console.WriteLine("Moving...");
    }
}

// Level 3
public class Mammal : Animal
{
    public virtual void ProduceMilk()
    {
        Console.WriteLine("Producing milk...");
    }
}

// Level 4
public class Dog : Mammal
{
    public override void Move()
    {
        Console.WriteLine("Running on four legs...");
    }
    
    public void Bark()
    {
        Console.WriteLine("Woof!");
    }
}

// Usage - Dog has access to all ancestor members
Dog dog = new Dog();
dog.Breathe();       // From LivingThing
dog.Move();          // From Animal (overridden in Dog)
dog.ProduceMilk();   // From Mammal
dog.Bark();          // From Dog

Transitive Nature of Inheritance

Although a class can inherit from only one base class (single inheritance), inheritance is transitive. This means that if class C inherits from class B, and class B inherits from class A, then class C inherits the members declared in both class B and class A. This property allows for deeper hierarchies and further code reuse.

Example - Vehicle Management System:

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

    public void StartEngine()
    {
        Console.WriteLine("Engine started.");
    }

    public void StopEngine()
    {
        Console.WriteLine("Engine stopped.");
    }
}

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

    public void OpenTrunk()
    {
        Console.WriteLine("Trunk opened.");
    }

    public void HonkHorn()
    {
        Console.WriteLine("Horn honked.");
    }

    public void LockDoors()
    {
        Console.WriteLine("Doors locked.");
    }
}

public class ElectricCar : Car
{
    public int BatteryCapacity { get; set; }

    public void ChargeBattery()
    {
        Console.WriteLine("Battery charging.");
    }

    public void DisplayBatteryStatus()
    {
        Console.WriteLine("Battery status displayed.");
    }
}

public class CombustionEngineCar : Car
{
    public int FuelCapacity { get; set; }

    public void Refuel()
    {
        Console.WriteLine("Car refueled.");
    }

    public void CheckOilLevel()
    {
        Console.WriteLine("Oil level checked.");
    }
}

In this example:

  • Vehicle class: The base class contains common properties and methods for all vehicles
  • Car class: Inherits from Vehicle and adds car-specific members
  • ElectricCar class: Inherits from Car (and transitively from Vehicle), adding electric-specific members
  • CombustionEngineCar class: Also inherits from Car (and transitively from Vehicle), adding combustion-specific members

The ElectricCar and CombustionEngineCar classes inherit members from both Car and Vehicle, demonstrating the transitive nature of inheritance.

Hierarchical Inheritance

Multiple classes inherit from the same base class:

public class BankAccount
{
    public string AccountNumber { get; set; }
    public decimal Balance { get; protected set; }
    
    public virtual void Deposit(decimal amount)
    {
        Balance += amount;
    }
    
    public virtual bool Withdraw(decimal amount)
    {
        if (Balance >= amount)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
}

public class SavingsAccount : BankAccount
{
    public decimal InterestRate { get; set; }
    
    public void ApplyInterest()
    {
        Balance += Balance * InterestRate;
    }
}

public class CheckingAccount : BankAccount
{
    public decimal OverdraftLimit { get; set; }
    
    public override bool Withdraw(decimal amount)
    {
        if (Balance + OverdraftLimit >= amount)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
}

public class BusinessAccount : BankAccount
{
    public string BusinessName { get; set; }
    public List<string> AuthorizedSigners { get; set; }
}

Polymorphism Through Inheritance

Polymorphism allows objects of different types to be treated through a common interface.

Runtime Polymorphism

public abstract class PaymentMethod
{
    public abstract void ProcessPayment(decimal amount);
}

public class CreditCard : PaymentMethod
{
    public string CardNumber { get; set; }
    
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment: ${amount}");
        // Credit card processing logic
    }
}

public class PayPal : PaymentMethod
{
    public string Email { get; set; }
    
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing PayPal payment: ${amount}");
        // PayPal processing logic
    }
}

public class BankTransfer : PaymentMethod
{
    public string AccountNumber { get; set; }
    
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing bank transfer: ${amount}");
        // Bank transfer logic
    }
}

// Polymorphic usage
public class PaymentProcessor
{
    public void ProcessTransaction(PaymentMethod payment, decimal amount)
    {
        payment.ProcessPayment(amount); // Calls appropriate implementation
    }
}

// Usage
PaymentProcessor processor = new PaymentProcessor();
processor.ProcessTransaction(new CreditCard(), 100.00m);
processor.ProcessTransaction(new PayPal(), 50.00m);
processor.ProcessTransaction(new BankTransfer(), 200.00m);

Collections of Base Type

List<PaymentMethod> payments = new List<PaymentMethod>
{
    new CreditCard { CardNumber = "1234-5678" },
    new PayPal { Email = "[email protected]" },
    new BankTransfer { AccountNumber = "987654321" }
};

// Process all payments polymorphically
foreach (var payment in payments)
{
    payment.ProcessPayment(100.00m);
}

Common Inheritance Scenarios

Scenario 1: Employee Management System

Requirement: Model different types of employees with varying salary calculations

Solution:

public abstract class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public DateTime HireDate { get; set; }
    
    public abstract decimal CalculateSalary();
    
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"ID: {EmployeeId}, Name: {Name}");
    }
}

public class HourlyEmployee : Employee
{
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }
    
    public override decimal CalculateSalary()
    {
        return HourlyRate * HoursWorked;
    }
}

public class SalariedEmployee : Employee
{
    public decimal AnnualSalary { get; set; }
    
    public override decimal CalculateSalary()
    {
        return AnnualSalary / 12; // Monthly salary
    }
}

public class CommissionEmployee : SalariedEmployee
{
    public decimal CommissionRate { get; set; }
    public decimal SalesAmount { get; set; }
    
    public override decimal CalculateSalary()
    {
        return base.CalculateSalary() + (SalesAmount * CommissionRate);
    }
}

Scenario 2: Document Processing System

Requirement: Handle different document types with common operations

public abstract class Document
{
    public string Title { get; set; }
    public string Author { get; set; }
    public DateTime CreatedDate { get; set; }
    
    public abstract void Open();
    public abstract void Save();
    
    public virtual void Print()
    {
        Console.WriteLine($"Printing: {Title}");
    }
}

public class TextDocument : Document
{
    public string Content { get; set; }
    
    public override void Open()
    {
        Console.WriteLine($"Opening text document: {Title}");
    }
    
    public override void Save()
    {
        Console.WriteLine($"Saving text document: {Title}");
    }
}

public class SpreadsheetDocument : Document
{
    public int Rows { get; set; }
    public int Columns { get; set; }
    
    public override void Open()
    {
        Console.WriteLine($"Opening spreadsheet: {Title}");
    }
    
    public override void Save()
    {
        Console.WriteLine($"Saving spreadsheet: {Title}");
    }
    
    public void CalculateFormulas()
    {
        Console.WriteLine("Calculating formulas...");
    }
}

public class PresentationDocument : Document
{
    public int SlideCount { get; set; }
    
    public override void Open()
    {
        Console.WriteLine($"Opening presentation: {Title}");
    }
    
    public override void Save()
    {
        Console.WriteLine($"Saving presentation: {Title}");
    }
    
    public void PlaySlideshow()
    {
        Console.WriteLine("Playing slideshow...");
    }
}

Scenario 3: Game Character System

Requirement: Create a flexible character system for a game

public abstract class GameCharacter
{
    public string Name { get; set; }
    public int Health { get; set; }
    public int Level { get; set; }
    
    public abstract void Attack();
    public abstract void Defend();
    
    public virtual void LevelUp()
    {
        Level++;
        Health += 10;
        Console.WriteLine($"{Name} leveled up to {Level}!");
    }
}

public class Warrior : GameCharacter
{
    public int Strength { get; set; }
    
    public override void Attack()
    {
        Console.WriteLine($"{Name} swings sword! Damage: {Strength * 2}");
    }
    
    public override void Defend()
    {
        Console.WriteLine($"{Name} raises shield! Defense increased.");
    }
    
    public void BattleCry()
    {
        Console.WriteLine($"{Name}: For honor!");
    }
}

public class Mage : GameCharacter
{
    public int Mana { get; set; }
    
    public override void Attack()
    {
        if (Mana >= 10)
        {
            Console.WriteLine($"{Name} casts fireball!");
            Mana -= 10;
        }
    }
    
    public override void Defend()
    {
        Console.WriteLine($"{Name} creates magic barrier!");
    }
    
    public void Meditate()
    {
        Mana += 20;
        Console.WriteLine($"{Name} meditates and restores mana.");
    }
}

public class Archer : GameCharacter
{
    public int Accuracy { get; set; }
    
    public override void Attack()
    {
        Console.WriteLine($"{Name} shoots arrow! Accuracy: {Accuracy}%");
    }
    
    public override void Defend()
    {
        Console.WriteLine($"{Name} dodges!");
    }
}

Scenario 4: Shape Drawing Application

Requirement: Implement geometric shapes with area and perimeter calculations

public abstract class Shape
{
    public string Color { get; set; }
    
    public abstract double CalculateArea();
    public abstract double CalculatePerimeter();
    
    public virtual void Draw()
    {
        Console.WriteLine($"Drawing a {Color} {GetType().Name}");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    
    public override double CalculatePerimeter()
    {
        return 2 * Math.PI * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
    
    public override double CalculatePerimeter()
    {
        return 2 * (Width + Height);
    }
}

public class Triangle : Shape
{
    public double SideA { get; set; }
    public double SideB { get; set; }
    public double SideC { get; set; }
    
    public override double CalculateArea()
    {
        // Using Heron's formula
        double s = (SideA + SideB + SideC) / 2;
        return Math.Sqrt(s * (s - SideA) * (s - SideB) * (s - SideC));
    }
    
    public override double CalculatePerimeter()
    {
        return SideA + SideB + SideC;
    }
}

Scenario 5: Vehicle Rental System

Requirement: Manage different vehicle types with varying rental rates

public abstract class RentalVehicle
{
    public string VehicleId { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    
    public abstract decimal CalculateDailyRate();
    
    public virtual decimal CalculateRentalCost(int days)
    {
        return CalculateDailyRate() * days;
    }
}

public class EconomyCar : RentalVehicle
{
    public override decimal CalculateDailyRate()
    {
        return 35.00m;
    }
}

public class LuxuryCar : RentalVehicle
{
    public bool HasChauffeur { get; set; }
    
    public override decimal CalculateDailyRate()
    {
        decimal baseRate = 150.00m;
        return HasChauffeur ? baseRate + 100.00m : baseRate;
    }
}

public class Truck : RentalVehicle
{
    public double PayloadCapacity { get; set; }
    
    public override decimal CalculateDailyRate()
    {
        return 75.00m + (decimal)(PayloadCapacity * 0.10);
    }
}

public class Motorcycle : RentalVehicle
{
    public int EngineSize { get; set; }
    
    public override decimal CalculateDailyRate()
    {
        return EngineSize > 600 ? 60.00m : 40.00m;
    }
}

Best Practices

1. Favor Composition Over Inheritance

// ❌ Don't: Deep inheritance hierarchies
public class Animal { }
public class Mammal : Animal { }
public class Carnivore : Mammal { }
public class Feline : Carnivore { }
public class Lion : Feline { }

// ✅ Do: Use composition for flexibility
public class Lion
{
    private readonly IMovementBehavior _movement;
    private readonly IDietBehavior _diet;
    
    public Lion(IMovementBehavior movement, IDietBehavior diet)
    {
        _movement = movement;
        _diet = diet;
    }
}

2. Keep Inheritance Hierarchies Shallow

Recommended: 2-3 levels maximum
├─ Base
    ├─ Derived1
    └─ Derived2

Avoid: Deep hierarchies (4+ levels)
├─ Level1
    ├─ Level2
        ├─ Level3
            ├─ Level4
                └─ Level5 (too deep!)

3. Use Abstract Classes for Common Behavior

// ✅ Good: Abstract base with common functionality
public abstract class Repository<T>
{
    protected readonly DbContext _context;
    
    protected Repository(DbContext context)
    {
        _context = context;
    }
    
    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }
    
    public abstract Task<IEnumerable<T>> GetAllAsync();
}

4. Follow the Liskov Substitution Principle

// ✅ Derived classes should be substitutable for base classes
public void ProcessShape(Shape shape)
{
    double area = shape.CalculateArea(); // Works for any Shape derivative
    Console.WriteLine($"Area: {area}");
}

// Works correctly with any shape
ProcessShape(new Circle { Radius = 5 });
ProcessShape(new Rectangle { Width = 4, Height = 6 });

5. Use Sealed When Appropriate

// Seal classes that shouldn't be extended
public sealed class SecurityToken
{
    // Implementation that must not be altered
}

// Seal overrides to prevent further modification
public class CustomList : BaseList
{
    public sealed override void Add(object item)
    {
        // Final implementation
    }
}

6. Document Inheritance Contracts

/// <summary>
/// Base class for all database operations.
/// Derived classes must implement connection-specific logic.
/// </summary>
public abstract class DatabaseRepository
{
    /// <summary>
    /// Connects to the database.
    /// Override to provide connection-specific implementation.
    /// </summary>
    protected abstract void Connect();
    
    /// <summary>
    /// Executes a query. Do not override unless absolutely necessary.
    /// </summary>
    public virtual void ExecuteQuery(string query)
    {
        Connect();
        // Base implementation
    }
}

7. Avoid Protected Fields

// ❌ Don't: Protected fields break encapsulation
public class BaseClass
{
    protected int _value;
}

// ✅ Do: Use protected properties
public class BaseClass
{
    private int _value;
    protected int Value
    {
        get => _value;
        set => _value = value;
    }
}

8. Make Methods Virtual Only When Necessary

// ❌ Don't: Make everything virtual by default
public class BaseClass
{
    public virtual void Method1() { }
    public virtual void Method2() { }
    public virtual void Method3() { }
}

// ✅ Do: Only make methods virtual when overriding is expected
public class BaseClass
{
    public void FinalMethod() { }  // Cannot override
    public virtual void ExtensibleMethod() { }  // Can override
}

9. Use Base Class References for Polymorphism

// ✅ Good: Use base class references
List<Employee> employees = new List<Employee>
{
    new HourlyEmployee(),
    new SalariedEmployee(),
    new CommissionEmployee()
};

foreach (Employee emp in employees)
{
    Console.WriteLine(emp.CalculateSalary());
}

10. Implement Proper Dispose Pattern for Inheritance

public class BaseResource : IDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose managed resources
            }
            // Dispose unmanaged resources
            _disposed = true;
        }
    }
}

public class DerivedResource : BaseResource
{
    private bool _disposed = false;
    
    protected override void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose derived class managed resources
            }
            _disposed = true;
        }
        base.Dispose(disposing);
    }
}

Advanced Inheritance Patterns

Template Method Pattern

Define the skeleton of an algorithm in a base class, letting derived classes override specific steps:

public abstract class DataProcessor
{
    public void ProcessData()
    {
        LoadData();
        ValidateData();
        TransformData();
        SaveData();
    }
    
    protected abstract void LoadData();
    protected abstract void TransformData();
    
    protected virtual void ValidateData()
    {
        Console.WriteLine("Performing basic validation...");
    }
    
    protected virtual void SaveData()
    {
        Console.WriteLine("Saving data...");
    }
}

public class CsvDataProcessor : DataProcessor
{
    protected override void LoadData()
    {
        Console.WriteLine("Loading CSV data...");
    }
    
    protected override void TransformData()
    {
        Console.WriteLine("Transforming CSV data...");
    }
}

public class XmlDataProcessor : DataProcessor
{
    protected override void LoadData()
    {
        Console.WriteLine("Loading XML data...");
    }
    
    protected override void TransformData()
    {
        Console.WriteLine("Transforming XML data...");
    }
    
    protected override void ValidateData()
    {
        base.ValidateData();
        Console.WriteLine("Performing XML schema validation...");
    }
}

Factory Method Pattern

Define an interface for creating objects, but let derived classes decide which class to instantiate:

public abstract class DocumentCreator
{
    public abstract IDocument CreateDocument();
    
    public void OpenDocument()
    {
        IDocument doc = CreateDocument();
        doc.Open();
    }
}

public class PdfDocumentCreator : DocumentCreator
{
    public override IDocument CreateDocument()
    {
        return new PdfDocument();
    }
}

public class WordDocumentCreator : DocumentCreator
{
    public override IDocument CreateDocument()
    {
        return new WordDocument();
    }
}

Strategy Pattern with Inheritance

Encapsulate algorithms in a hierarchy of classes:

public abstract class SortingStrategy
{
    public abstract void Sort(int[] array);
    
    protected void Swap(int[] array, int i, int j)
    {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

public class BubbleSort : SortingStrategy
{
    public override void Sort(int[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            for (int j = 0; j < array.Length - 1; j++)
            {
                if (array[j] > array[j + 1])
                {
                    Swap(array, j, j + 1);
                }
            }
        }
    }
}

public class QuickSort : SortingStrategy
{
    public override void Sort(int[] array)
    {
        QuickSortHelper(array, 0, array.Length - 1);
    }
    
    private void QuickSortHelper(int[] array, int low, int high)
    {
        // Quick sort implementation
    }
}

Performance Considerations

Virtual Method Call Overhead

Virtual methods have a small performance cost due to virtual method table (vtable) lookups:

public class Base
{
    public virtual void Method() { }
}

public class Base
{
    public void Method() { }
}

Best Practice: Only use virtual when polymorphism is needed.

Sealed Classes and Methods

Sealing classes and methods can improve performance by allowing the JIT compiler to inline methods:

public sealed class OptimizedClass
{
    public void Method()
    {
        
    }
}

Deep Inheritance Hierarchies

Avoid deep inheritance hierarchies as they can impact:

  • Performance (more vtable lookups)
  • Maintainability (harder to understand)
  • Flexibility (tight coupling)
// ❌ Avoid: Deep hierarchy
Level1 → Level2 → Level3 → Level4 → Level5

// ✅ Prefer: Shallow hierarchy with composition
Base → Derived (with composed behaviors)

Memory and Object Lifetime

Constructor Chain Execution

Understanding the constructor chain is important for proper initialization:

public class GrandParent
{
    public GrandParent()
    {
        Console.WriteLine("1. GrandParent constructor");
    }
}

public class Parent : GrandParent
{
    public Parent()
    {
        Console.WriteLine("2. Parent constructor");
    }
}

public class Child : Parent
{
    public Child()
    {
        Console.WriteLine("3. Child constructor");
    }
}

// Creating Child executes all three constructors in order:
Child child = new Child();
// Output:
// 1. GrandParent constructor
// 2. Parent constructor
// 3. Child constructor

Destructor Chain Execution

Destructors execute in reverse order:

public class GrandParent
{
    ~GrandParent()
    {
        Console.WriteLine("3. GrandParent destructor");
    }
}

public class Parent : GrandParent
{
    ~Parent()
    {
        Console.WriteLine("2. Parent destructor");
    }
}

public class Child : Parent
{
    ~Child()
    {
        Console.WriteLine("1. Child destructor");
    }
}

Type Casting and Checking

Animal animal = new Dog();

if (animal is Dog)
{
    Console.WriteLine("It's a dog!");
}

// Pattern matching
if (animal is Dog dog)
{
    dog.Bark();
}

// Type casting
Dog myDog = (Dog)animal; // Throws exception if not a Dog
Dog safeDog = animal as Dog; // Returns null if not a Dog

// Safe casting with null check
if (animal as Dog is Dog castedDog)
{
    castedDog.Bark();
}

Design Guidelines

When to Use Inheritance

Use inheritance when:

  • There's a clear "is-a" relationship
  • You want to reuse implementation from a base class
  • The relationship is stable and unlikely to change
  • Derived classes need to override base class behavior

Example:

// Good: Clear "is-a" relationship
public class Vehicle { }
public class Car : Vehicle { }
public class Truck : Vehicle { }

When to Avoid Inheritance

Avoid inheritance when:

  • The relationship is "has-a" rather than "is-a" (use composition)
  • You need multiple inheritance (use interfaces)
  • The base class is likely to change frequently
  • The hierarchy would be more than 3 levels deep

Example:

// ❌ Bad: "has-a" relationship using inheritance
public class Engine { }
public class Car : Engine { } // Car is not an Engine

// ✅ Good: Use composition
public class Engine { }
public class Car
{
    private Engine _engine; // Car has an Engine
}

Prefer Interfaces for Contracts

// ✅ Good: Interface defines contract
public interface IRepository<T>
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
}

public class SqlRepository<T> : IRepository<T>
{
    // Implementation
}

Use Abstract Classes for Shared Implementation

// ✅ Good: Abstract class provides shared implementation
public abstract class RepositoryBase<T>
{
    protected readonly DbContext _context;
    
    protected RepositoryBase(DbContext context)
    {
        _context = context;
    }
    
    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }
    
    public abstract Task<IEnumerable<T>> GetAllAsync();
}

Quick Reference

Inheritance Syntax

// Basic inheritance
public class Derived : Base { }

// With interface implementation
public class Derived : Base, IInterface1, IInterface2 { }

// Abstract class
public abstract class Base
{
    public abstract void AbstractMethod();
    public virtual void VirtualMethod() { }
    public void ConcreteMethod() { }
}

// Sealed class
public sealed class Final : Base 
{
    public override void AbstractMethod() { }
}

Access Modifiers in Inheritance

ModifierSame ClassDerived ClassSame AssemblyOther Assembly
public
protected
internal✓ (same assembly)
protected internal
private protected✓ (same assembly)
private

Keywords Quick Reference

KeywordUsagePurpose
virtualBase class methodAllows overriding in derived classes
overrideDerived class methodOverrides base class virtual method
abstractBase class/methodMust be overridden in derived classes
sealedClass or methodPrevents inheritance or overriding
newDerived class memberHides base class member
baseDerived classAccesses base class members

Method Override Rules

// Base class must use virtual, abstract, or override
public class Base
{
    public virtual void Method1() { }
    public abstract void Method2();
}

// Derived class uses override
public class Derived : Base
{
    public override void Method1() { }
    public override void Method2() { }
}

// Cannot override non-virtual methods
public class Base
{
    public void Method() { } // Not virtual
}

public class Derived : Base
{
    // ❌ Error: Cannot override
    // public override void Method() { }
    
    // ✅ Can hide with 'new'
    public new void Method() { }
}

Conclusion

Class inheritance is a powerful feature in C# that enables code reuse, promotes logical organization, and supports polymorphic behavior.

Key Takeaways:

  • Inheritance creates "is-a" relationships between classes
  • A class can inherit from only one base class in C#
  • Use virtual and override keywords to enable polymorphism
  • Abstract classes define common behavior with required implementations
  • Sealed classes and methods prevent further inheritance or overriding
  • Access base class members using the base keyword
  • Keep inheritance hierarchies shallow (2-3 levels maximum)
  • Favor composition over inheritance for complex scenarios
  • Use interfaces for "can-do" relationships and inheritance for "is-a" relationships
  • Follow SOLID principles, especially Liskov Substitution Principle
  • Document inheritance contracts clearly for maintainability

Remember: Inheritance is a tool, not a goal. Use it when it creates clearer, more maintainable code, but don't force inheritance relationships where composition or interfaces would be more appropriate. Always consider whether an "is-a" relationship truly exists before using inheritance.


Additional Resources

Official Documentation

Design Principles

Learning Resources

Advanced Topics

Community Resources

Books and References

  • "C# in Depth" by Jon Skeet
  • "CLR via C#" by Jeffrey Richter
  • "Design Patterns: Elements of Reusable Object-Oriented Software" by Gang of Four
  • "Clean Code" by Robert C. Martin