Sealed Classes
Definition
A sealed class is a class that cannot be inherited. Marking a class with the sealed keyword prevents derivation and enables the JIT compiler to optimize method calls, type checks, and array operations.
public sealed class Husky : Animal
{
public override void DoNothing() { }
public override int GetAge() => 11;
}
See OOP for sealed class syntax and basic usage. This page focuses on performance implications.
Why Sealed Classes Are Faster
1. Devirtualization (Direct Method Calls)
When calling a virtual method on an open class, the runtime must use virtual dispatch — it looks up the method table to find the correct override. This involves extra mov instructions in the JIT-compiled assembly.
With a sealed class, the JIT knows there are no further overrides. It can replace the indirect call with a direct call to the method's memory address — devirtualization.
Benchmark (BenchmarkDotNet):
| Method | Mean | Notes |
|---|---|---|
Sealed_VoidMethod | 0.0030 ns | Direct call — nearly free |
Open_VoidMethod | 0.6350 ns | Virtual dispatch overhead |
// JIT sees sealed → devirtualizes → direct call
[Benchmark]
public void Sealed_VoidMethod() => _husky.DoNothing();
// JIT sees open class → must use virtual dispatch
[Benchmark]
public void Open_VoidMethod() => _bear.DoNothing();
The same pattern holds for methods with return values:
| Method | Mean |
|---|---|
Sealed_IntMethod | 0.0857 ns |
Open_IntMethod | 0.5081 ns |
If the JIT can determine the exact type at compile time (e.g., the object is created and used within the same method), it will devirtualize even open classes. The performance gap disappears in that case. Sealed classes guarantee this optimization regardless of context.
2. Faster Type Checking and Casting
The is and as operators are significantly faster on sealed types because the runtime only needs to compare against a single concrete type. For open classes, it must traverse the entire type hierarchy.
Type checking (is):
| Method | Mean |
|---|---|
Sealed_TypeCheck | 0.3880 ns |
Open_TypeCheck | 2.0330 ns |
Casting (as):
| Method | Mean |
|---|---|
Sealed_Casting | 0.2757 ns |
Open_Casting | 2.0827 ns |
private readonly Animal _animal = new Animal();
// Sealed: single type comparison
[Benchmark]
public Husky? Sealed_Casting() => _animal as Husky;
// Open: must check full hierarchy
[Benchmark]
public Bear? Open_Casting() => _animal as Bear;
3. Array Covariance Check Elimination
Storing elements in arrays of reference types requires a covariance check — the runtime must verify the element type is assignable. Sealed classes skip this check because there are no subtypes.
| Method | Mean |
|---|---|
Sealed_AddToArray | 4.322 ns |
Open_AddToArray | 5.629 ns |
4. Static Method Calls — No Difference
Static methods cannot be overridden, so there is no virtual dispatch regardless of whether the class is sealed:
| Method | Mean |
|---|---|
Sealed_StaticMethod | 0.0183 ns |
Open_StaticMethod | 0.0340 ns |
5. ToString() — Minor Improvement
| Method | Mean |
|---|---|
Sealed_ToString | 5.731 ns |
Open_ToString | 7.149 ns |
Unreachable Code Detection
The compiler can detect impossible casts and type checks on sealed classes at compile time:
public interface IFlyingAnimal { }
// Compile error: Husky is sealed and doesn't implement IFlyingAnimal
var flying = _animal as Husky; // compiler knows this can never be IFlyingAnimal
// Warning: condition is always false
if (_animal is Husky && _animal is IFlyingAnimal) { } // unreachable
This helps reduce dead code and catch logic errors early.
Trade-offs
Mocking Difficulty
Sealed classes cannot be mocked by most mocking frameworks (e.g., Moq):
// System.NotSupportedException — Type to mock must be an interface,
// an abstract or non-sealed class
var mock = new Mock<Husky>();
Workaround: Code to an interface instead of the concrete sealed type, then mock the interface.
When to Seal
| Scenario | Seal? |
|---|---|
| Class has no logical reason to be inherited | Yes |
| Performance-sensitive code paths (hot paths) | Yes |
| Security-critical classes that must not be extended | Yes |
| Class needs to be mocked in unit tests | Consider interface abstraction first |
| Class is part of a public API designed for extension | No |
Summary of Performance Impact
| Operation | Sealed Advantage |
|---|---|
| Virtual method calls | Devirtualized — direct call, no vtable lookup |
Type checking (is) | ~5x faster |
Casting (as) | ~7.5x faster |
| Array operations | ~20% faster (no covariance check) |
| Static methods | No difference |
ToString() | Minor improvement |
Mark classes sealed by default unless you intentionally design for inheritance. The performance benefit is free, and it communicates design intent clearly.
Common Pitfalls
- Sealing too early in a public API — if consumers need to extend your types, sealing breaks compatibility. Consider your API surface before sealing.
- Mocking friction — sealed classes require interface abstractions for testability. Factor this into your design.
- Assuming static methods benefit — static methods are already non-virtual; sealing provides no additional gain for them.
Interview Questions
Q: Why are sealed classes faster than open classes in C#?
A: The JIT compiler can devirtualize method calls on sealed types — it replaces indirect vtable lookups with direct calls because it knows there are no further overrides. Sealed types also enable faster type checks (is/as) since the runtime only compares against a single type instead of traversing the hierarchy.
Q: Should you seal classes by default? A: Yes, unless the class is intentionally designed for inheritance. Sealing communicates that the class is a leaf type, enables JIT optimizations, and prevents unintended subclassing. Most classes in a typical codebase are never inherited.
Q: What is the downside of using sealed classes? A: Sealed classes cannot be inherited or easily mocked by standard mocking frameworks. To maintain testability, code should depend on interfaces rather than concrete sealed types.
Q: How does sealing affect array operations? A: Storing reference types in arrays requires a runtime covariance check to verify type compatibility. Sealed classes skip this check because they have no subtypes, resulting in slightly faster array assignments.
Q: Does the JIT always use virtual dispatch for non-sealed classes? A: No. If the JIT can determine the exact runtime type at compile time (e.g., the object is created and used locally within a single method), it can devirtualize the call even for non-sealed classes. Sealing guarantees this optimization regardless of context.