Abstract Classes
An abstract class cannot be instantiated and can require members from its children, while sealed closes a class to inheritance entirely.
abstract 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
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: 12When 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.
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.
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.
// 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