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.
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; }
}
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.
| Technique | Keyword | Dispatch | When to use |
| Overriding | override | Runtime (virtual) | Specialize a virtual contract |
| Hiding | new | Compile-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.