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