C# sealed Classes
A sealed class is a C# class that cannot be inherited from. You use it when a type is complete as designed and should not become someone else’s base class. Sealing matters because inheritance is a public design promise: once other code derives from your class, changing its behavior becomes much harder.
Overview: How Sealed Classes Work
In C#, classes are inheritable by default unless they are static or sealed. When you mark a class with sealed, the compiler allows normal object creation, normal methods, properties, interfaces, constructors, and fields, but it rejects any class that tries to derive from it. A sealed class can still inherit from another class, and it can still implement interfaces. The restriction only points downward: no further derived classes are allowed.
Sealing is not mainly about making code shorter. It is a design boundary. If a class is not designed for inheritance, derived classes can easily break its assumptions. They might override virtual members in surprising ways, skip important base behavior, depend on constructor timing, or observe internal state in an order the original author never intended to support. Marking the class sealed tells readers and the compiler that the inheritance chain ends there.
The keyword is common on value-like reference types, security-sensitive types, immutable domain objects, adapters, exceptions with fixed behavior, and concrete implementations that are meant to be used through an interface. For example, SqlInvoiceRepository might implement IInvoiceRepository and be sealed because callers should depend on the interface, not subclass the repository. This keeps extension points explicit.
There is also a related form: sealed override. A derived class can override a virtual member and then seal that specific override. Later classes may still inherit from the derived class, but they cannot override that member again. This is useful when one behavior must be finalized while the rest of the class can remain extensible.
Under the hood, the compiler records the sealed restriction in type metadata. If another compilation tries to use the sealed type as a base class, compilation fails before the program runs. The CLR also understands this metadata when loading types. Because a sealed class has no derived runtime types, the JIT compiler may sometimes optimize virtual calls or type checks more aggressively, although you should seal for design clarity first and treat performance as a possible bonus.
Syntax
sealed class FinalType : BaseType, ISomeInterface
{
public void DoWork()
{
Console.WriteLine("Work complete");
}
}
class DerivedType : SomeBase
{
public sealed override string Describe()
{
return "Final description";
}
}
| Part | Meaning |
|---|---|
sealed class |
Declares a class that can be instantiated but cannot be used as a base class. |
: BaseType |
A sealed class may still inherit from one base class. |
ISomeInterface |
A sealed class may implement one or more interfaces. |
sealed override |
Overrides an inherited virtual member and prevents later derived classes from overriding that member again. |
static class |
A static class is implicitly sealed and cannot be instantiated; it is a different concept. |
Examples
Example 1: A Simple Sealed Class
using System;
using System.Globalization;
sealed class TemperatureReading
{
public TemperatureReading(string city, double celsius)
{
City = city;
Celsius = celsius;
}
public string City { get; }
public double Celsius { get; }
public double Fahrenheit()
{
return Celsius * 9 / 5 + 32;
}
public string Summary()
{
string fahrenheit = Fahrenheit().ToString("0.0", CultureInfo.InvariantCulture);
return $"{City}: {fahrenheit} F";
}
}
class Program
{
static void Main()
{
TemperatureReading reading = new TemperatureReading("Denver", 21.5);
Console.WriteLine(reading.Summary());
}
}
Output:
Denver: 70.7 F
TemperatureReading is a complete concrete type. Code can create it with new, call its methods, and read its properties. The only forbidden operation is deriving another class from TemperatureReading. That is appropriate here because the class represents one fixed data concept rather than a framework for related temperature types.
Example 2: Sealing a Concrete Implementation
using System;
using System.Globalization;
abstract class PaymentMethod
{
public abstract string Name { get; }
public abstract decimal Fee(decimal amount);
public string BuildLine(decimal amount)
{
string fee = Fee(amount).ToString("0.00", CultureInfo.InvariantCulture);
return $"{Name} fee: ${fee}";
}
}
sealed class CardPayment : PaymentMethod
{
public override string Name => "Card";
public override decimal Fee(decimal amount)
{
return amount * 0.029m + 0.30m;
}
}
class Program
{
static void Main()
{
PaymentMethod payment = new CardPayment();
Console.WriteLine(payment.BuildLine(100m));
}
}
Output:
Card fee: $3.20
A sealed class can still participate in polymorphism. CardPayment inherits from PaymentMethod and supplies the required overrides, so a PaymentMethod variable can hold it. Sealing only says that CardPayment itself is the final class in that branch.
Example 3: Sealing One Override
using System;
class AuditEvent
{
public virtual string Severity()
{
return "Info";
}
public virtual string Message()
{
return "Audit event";
}
}
class SecurityEvent : AuditEvent
{
public sealed override string Severity()
{
return "High";
}
public override string Message()
{
return "Security event";
}
}
class LoginFailureEvent : SecurityEvent
{
public override string Message()
{
return "Failed login";
}
}
class Program
{
static void Main()
{
AuditEvent auditEvent = new LoginFailureEvent();
Console.WriteLine(auditEvent.Severity());
Console.WriteLine(auditEvent.Message());
}
}
Output:
High
Failed login
SecurityEvent seals only the Severity override. LoginFailureEvent can still inherit from SecurityEvent and override Message, but it must keep the inherited High severity. This gives you finer control than sealing the entire class.
How It Works Step by Step
- The compiler reads the
sealedmodifier and records that the class cannot be a base class. - Code may still create instances of the sealed class if its constructor is accessible.
- Code may call inherited, overridden, interface, and ordinary members normally.
- If another class writes
class Child : SealedParent, the compiler reports an error. - If a member is marked
sealed override, the compiler allows that override but rejects later attempts to override the same member. - At runtime, the object is still an ordinary reference-type object with its fields, method table, and runtime type information.
- Because no more derived runtime type can exist below a sealed class, runtime dispatch has fewer possible targets for that branch of the hierarchy.
The important point is that sealed does not make an object read-only, static, private, or impossible to mock by interface. It only controls inheritance. Mutability is controlled by fields, properties, setters, and methods. Visibility is controlled by access modifiers. Whether code can substitute another implementation is usually controlled by interfaces or abstract base types.
Common Mistakes
Mistake 1: Trying to Inherit From a Sealed Class
sealed class CsvReport
{
public string Extension => ".csv";
}
class CustomCsvReport : CsvReport
{
}
This does not compile because CsvReport is sealed. If you need variation, use composition or depend on an interface instead of subclassing the sealed class.
using System;
interface IReportFormat
{
string Extension { get; }
}
sealed class CsvReport : IReportFormat
{
public string Extension => ".csv";
}
sealed class ReportExporter
{
private readonly IReportFormat format;
public ReportExporter(IReportFormat format)
{
this.format = format;
}
public void PrintExtension()
{
Console.WriteLine(format.Extension);
}
}
class Program
{
static void Main()
{
ReportExporter exporter = new ReportExporter(new CsvReport());
exporter.PrintExtension();
}
}
Output:
.csv
Mistake 2: Thinking Sealed Means Immutable
using System;
sealed class Counter
{
public int Value { get; private set; }
public void Increment()
{
Value++;
}
}
class Program
{
static void Main()
{
Counter counter = new Counter();
counter.Increment();
counter.Increment();
Console.WriteLine(counter.Value);
}
}
Output:
2
Counter is sealed, but it is still mutable because Increment changes its state. To make a sealed class immutable, design it with read-only fields, get-only properties, and methods that return new values rather than changing the current object.
Mistake 3: Trying to Seal a Non-Override Method
class Formatter
{
public sealed string Format(string value)
{
return value.Trim();
}
}
This does not compile because sealed on a member is only valid with override. If a method should not be replaced, simply leave it non-virtual.
using System;
class Formatter
{
public string Format(string value)
{
return value.Trim();
}
}
class Program
{
static void Main()
{
Formatter formatter = new Formatter();
Console.WriteLine(formatter.Format(" clean "));
}
}
Output:
clean
Best Practices
- Seal classes that are concrete implementation details and are not intentionally designed as base classes.
- Prefer interfaces for extensibility when callers need to swap implementations without inheriting from your class.
- Use
sealed overridewhen a derived class must finalize one inherited behavior but can still allow other customization. - Do not seal a class just to make testing difficult; expose useful abstractions such as interfaces when substitution is required.
- Remember that
sealeddoes not imply immutable. Use read-only design separately. - Be cautious about sealing public library types after release because existing consumers may already inherit from them.
- Seal small value-object-style reference types when inheritance would weaken equality, validation, or formatting rules.
- Leave classes unsealed only when you are prepared to support derived classes as part of the contract.
Practice Exercises
- Create a sealed
ApiKeyclass with a get-onlyValueproperty and aMaskedmethod that shows only the last four characters. - Create an abstract
ShippingCalculatorwith a sealedFlatRateShippingCalculatorimplementation. Store it in a base-class variable and print the calculated cost. - Create a base
Documentclass with a virtualKindmethod. Override and sealKindinInvoiceDocument, then add a derived class that overrides a different virtual method.
Summary
- A
sealedclass can be instantiated but cannot be inherited from. - Sealed classes can still inherit from one base class and implement interfaces.
sealed overridelocks down one overridden member while leaving the class itself inheritable.- The compiler enforces sealed inheritance rules before the program runs.
- Sealing is a design decision first; possible runtime optimization is secondary.
sealeddoes not mean immutable, static, private, or untestable.- Use interfaces or composition when you need variation around a sealed concrete class.
