Abstract Classes

An abstract class cannot be instantiated and can require members from its children, while sealed closes a class to inheritance entirely.

Syntaxabstract class Shape { public abstract int Area(); }

Some base classes are not meant to be created directly. A Shape that does not say what shape it is has no area to report. Marking the class abstract says exactly that: this class exists to be inherited from, and new Shape() is a compile error.

Abstract members

An abstract member has no body at all, just a signature and a semicolon. Every non-abstract class that derives from the class must supply one with override, or it will not compile. That is the useful part: the base class can call a method it has not written, confident that whichever concrete child exists has written it.

Abstract versus virtual

Both can be overridden. The difference is whether there is a fallback. virtual has a body, so a child may leave it alone. abstract has no body, so a child must supply one. Choose abstract when no sensible default exists, and virtual when one does.

An abstract class is still a class. It can have fields, constructors, private helpers and ordinary methods. Only the abstract members are left unwritten.

sealed

sealed is the opposite instruction. A sealed class cannot be inherited from. A sealed override member cannot be overridden again. Sealing a class you never designed for inheritance is the honest default, because it stops someone depending on behaviour you intend to change.

Example

Example · csharp
Shape[] shapes = [new Square(3), new Box(3, 4)];

foreach (Shape shape in shapes)
{
    Console.WriteLine($"{shape.Name}: {shape.Area()}");
}

abstract class Shape
{
    // abstract: no body, so every concrete child must supply one
    public abstract int Area();

    // virtual: there is a body, so a child may replace it or leave it
    public virtual string Name => "shape";
}

// sealed stops anything inheriting from Square
sealed class Square(int side) : Shape
{
    public override int Area() => side * side;

    public override string Name => "square";
}

// Box supplies Area but leaves Name, so it keeps the one from Shape
sealed class Box(int width, int height) : Shape
{
    public override int Area() => width * height;
}

// Output:
// square: 9
// shape: 12

When to use it

  • Force every payment provider class to implement Charge while sharing the same validation code.
  • Stop callers constructing a base HttpHandler that has no transport behind it.
  • Seal a value-like class so nobody subclasses it and breaks its equality rules.

More examples

An abstract class with a constructor and a shared method

This is the common shape for an abstract class: a concrete method that owns the order of the steps, and one abstract hole for the part that differs. Reach for it when every variation shares a workflow and differs in one place, such as several exporters that all need the same header and footer.

Example · csharp
abstract class Exporter
{
    // abstract classes can have constructors; children call them with base(...)
    protected Exporter(string name)
    {
        Name = name;
    }

    public string Name { get; }

    // The shared steps are written once here. The varying step is abstract.
    public string Run(string[] rows)
    {
        string body = FormatBody(rows);
        return $"{Name}: {body}";
    }

    // protected abstract: children must write it, outside code cannot call it
    protected abstract string FormatBody(string[] rows);
}

sealed class CsvExporter : Exporter
{
    public CsvExporter() : base("csv")
    {
    }

    protected override string FormatBody(string[] rows) => string.Join(",", rows);
}

Which member should be abstract, and which virtual

The decision rule in one file. If you cannot write a default that is correct for every child, make it abstract and let the compiler chase the children. If you can, make it virtual so the common case stays short.

Example · csharp
abstract class Job
{
    // No sensible default exists for a job's name, so it is abstract
    public abstract string Name { get; }

    // Three retries is a fine default, so this is virtual
    public virtual int Retries => 3;
}

sealed class EmailJob : Job
{
    public override string Name => "email";
    // Retries is not overridden, so it stays 3
}

sealed class BillingJob : Job
{
    public override string Name => "billing";
    public override int Retries => 10;
}

What abstract and sealed forbid

Both errors arrive at compile time, which is the point of using these keywords: the restriction is checked before the program ever runs. Keep this in mind when a library type refuses to be subclassed - it is sealed on purpose, and the way to extend it is composition or an extension member.

Example · csharp
// Shape shape = new Shape();
// Compiler error: you cannot create an instance of an abstract class.
// Create a Square or a Box instead.

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

sealed class Square(int side) : Shape
{
    public override int Area() => side * side;
}

// sealed class Tiny : Square { }
// Compiler error: Square is sealed, so nothing can derive from it.

Discussion

  • Be the first to comment on this lesson.