Leaving a class open to inheritance is a design decision, not a default you
can ignore.
The core idea
An unsealed class is a promise: every virtual member can be overridden
without breaking what the class guarantees. Most classes never meant to
make that promise. They're just unsealed by default, because that's what
class gives you unless you say otherwise.
Common mistake: treating sealed as "I don't want to think about
subclassing" rather than "this type's invariants would break if someone
could."
One override breaks the promise
Here's the promise, a BankAccount that refuses to go negative:
public class BankAccount
{
public decimal Balance { get; protected set; }
public virtual void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InvalidOperationException();
Balance -= amount;
}
}
And here's the override that breaks it:
public class RiskyAccount : BankAccount
{
public override void Withdraw(decimal amount)
{
Balance -= amount; // no check
}
}
Nothing here is exotic. It compiles cleanly, and RiskyAccount is a
perfectly legal BankAccount as far as the type system is concerned. Open
one with a balance of 100 and withdraw 500:
BankAccount account = new RiskyAccount(100m);
account.Withdraw(500m);
Console.WriteLine($"Balance: {account.Balance:F2}");
Real dotnet run output:
Balance: -400.00
The check on the left never ran. virtual was an open invitation, and
RiskyAccount took it.
Sealing turns a silent bug into a compile error
Without sealed, the code above compiles and produces a wrong answer at
runtime; nothing points you at the problem until it's already in
production. With sealed, the same mistake becomes something the compiler
catches before the code ever runs:
public sealed class BankAccount
{
public decimal Balance { get; protected set; }
public void Withdraw(decimal amount)
{
if (amount > Balance) throw new InvalidOperationException();
Balance -= amount;
}
}
public class RiskyAccount : BankAccount { }
// error CS0509: 'RiskyAccount': cannot derive
// from sealed type 'BankAccount'
Sealing also makes virtual on Withdraw pointless, and the compiler
enforces that too:
public sealed class BankAccount
{
public virtual void Withdraw(decimal amount) { /* ... */ }
}
// error CS0549: 'BankAccount.Withdraw(decimal)' is a new virtual
// member in sealed type 'BankAccount'
Both of those are real compiler errors, not paraphrased, verified against
a live build.
The takeaway
Seal by default. Unseal only when you intend to support inheritance. Every
unsealed class is an implicit promise to every future subclass that
overriding won't break its invariants. Most classes never meant to make
that promise, they're just unsealed because nobody had to actively choose
not to be.
Top comments (0)