<< Home

Chapter 8: Inheritance and Polymorphism

Inheritance lets a type reuse and extend another type; polymorphism lets code operate on a base type while dispatching behavior to the most specific implementation at runtime. This chapter shows how to declare hierarchies, control extensibility, and choose inheritance or composition wisely.

Base classes and derived classes

A class can inherit from one base class using the colon syntax. The derived class gains the base class members that are not private; it can add new members and specialize behavior. Constructors are not inherited; a derived constructor must chain to a base constructor explicitly or implicitly.

Declaring a base and deriving a subtype

The following example defines a base Shape and a derived Circle. The base provides common state; the derived adds specific state and behavior.

class Shape
{
  public string Name { get; }
  public Shape(string name) { Name = name; }
}

class Circle : Shape
{
  public double Radius { get; }
  public Circle(double radius) : base("Circle") { Radius = radius; }
}

When a derived constructor calls base(...) it ensures the base portion is initialized correctly; omitting an explicit call uses a parameterless base constructor if it exists.

Access modifiers in hierarchies

Members marked private are not visible to derived types; protected members are visible to derived types; internal members are visible within the assembly. You can combine protected internal or use private protected for finer control.

💡 Prefer protected only for members intended for derived classes; otherwise keep members private and expose behavior through methods or properties.

Virtual dispatch

Polymorphism in C# uses virtual dispatch. A base member marked virtual can be overridden in a derived type using override. You can prevent further overriding with sealed on an overriding member. This allows a library to be extensible where needed and fixed where stability matters.

Declaring and overriding virtual members

Mark base members as virtual only when you intend them to be replaced. Use override in derived classes to specialize behavior and optionally call the base implementation with base.Member().

class Shape
{
  public virtual double Area() { return 0.0; }
}

class Rectangle : Shape
{
  public double Width { get; }
  public double Height { get; }
  public Rectangle(double w, double h) { Width = w; Height = h; }
  public override double Area() { return Width * Height; }
}

If a derived override should be the last implementation in the chain, mark it sealed. This keeps the class derivable while locking that specific behavior.

class Square : Rectangle
{
  public Square(double side) : base(side, side) { }
  public sealed override double Area() { return base.Area(); }
}

Overriding properties and indexers

Properties and indexers participate in polymorphism. You can mark a property virtual and then override its accessors; the get and set bodies live together under the same declaration.

class Document
{
  public virtual string Title { get; set; } = "Untitled";
}

class Report : Document
{
  public override string Title { get; set; } = "Report";
}

Abstract classes and members

An abstract class cannot be instantiated; it can provide shared implementation and define abstract members that derived classes must implement. Abstract members have no body. Use an abstract base when you want to share code and enforce a contract together.

Defining abstract types

In this example the base Shape requires an Area implementation. Some derived shapes implement it directly; others can remain abstract until they provide enough information.

abstract class Shape
{
  public abstract double Area();
  public virtual string Describe() { return "Shape"; }
}

class Circle : Shape
{
  public double Radius { get; }
  public Circle(double r) { Radius = r; }
  public override double Area() { return Math.PI * Radius * Radius; }
}
⚠️ Prefer an abstract base only when you need shared code plus a required contract; if you only need a contract choose an interface.

Interfaces

An interface defines a contract that multiple types can implement. C# allows default interface members; you can provide a method body in the interface for shared behavior while still allowing implementations to override. This is useful for evolving contracts gradually.

Implementing interfaces

A type can implement many interfaces. Members are implicitly public; explicit interface implementation can hide members from the class surface when you only want them through the interface reference.

interface IPrintable
{
  void Print();
  string Header => "Header"; // default property body
  void PrintHeader() { Console.WriteLine(Header); } // default method body
}

class Invoice : IPrintable
{
  public void Print() { Console.WriteLine("Invoice"); }
  // inherits default PrintHeader unless overridden
}

Default members in an interface are inherited by implementers; the implementer can provide its own body with the same signature to replace the default.

Explicit interface implementations

Use explicit implementation when a member should not be visible on the class itself. Access it through an interface reference; this avoids name collisions and reduces the public surface.

interface IHasId { string Id { get; } }

class User : IHasId
{
  string IHasId.Id => "user:…"; // explicit implementation
}

Hiding vs override

Sometimes a derived class declares a member with the same signature as a base member. Using override participates in virtual dispatch; using the new modifier hides the base member. Hiding selects the member based on the static type of the reference; overriding selects based on the runtime type.

Comparison of behavior

The following table summarizes the differences between overriding and hiding; understand the dispatch rules to avoid surprises.

TechniqueKeywordDispatchWhen to use
OverridingoverrideRuntime (virtual)Specialize a virtual contract
HidingnewCompile-time (static)Replace a base member for new scenarios without changing virtual behavior

Code demonstration

Notice how calls route differently when the variable is typed as the base or derived class. The hidden method is chosen only when the static type is the derived class.

class Base
{
  public virtual void M() { Console.WriteLine("Base.M"); }
  public void N() { Console.WriteLine("Base.N"); }
}

class Derived : Base
{
  public override void M() { Console.WriteLine("Derived.M"); }
  public new void N() { Console.WriteLine("Derived.N"); }
}

Base b = new Derived();
Derived d = new Derived();

b.M();  // Derived.M (override)
b.N();  // Base.N (hidden, static binding)

d.M();  // Derived.M
d.N();  // Derived.N

When to prefer composition

Inheritance models an is-a relationship; composition models a has-a relationship. Prefer composition when you want to assemble behavior from independent parts, when you do not control a base class, or when a type hierarchy would become deep and rigid. Composition keeps coupling low and testing simple.

Delegation with composed services

In this example a ReportRenderer delegates to a IFormatter. You can swap implementations at runtime; you can combine behaviors without creating many subclasses.

interface IFormatter { string Format(string text); }

class PlainFormatter : IFormatter
{
  public string Format(string text) { return text; }
}

class MarkdownFormatter : IFormatter
{
  public string Format(string text) { return "**" + text + "**"; }
}

class ReportRenderer
{
  private readonly IFormatter _formatter;
  public ReportRenderer(IFormatter formatter) { _formatter = formatter; }
  public void Render(string text) { Console.WriteLine(_formatter.Format(text)); }
}

Composition can live alongside inheritance. You might have a small abstract base for minimal shared code and then compose services for variable behavior; this keeps the hierarchy shallow and extensible.