By default, classes are not sealed, meaning any class can inherit from them. A class should be sealed unless it is explicitly designed for inheritance – you can always remove the modifier later if needed. Beyond design concerns, sealing a class also has performance implications: the JIT can apply optimizations that slightly improve application performance.
Starting in .NET 7, a new analyzer is available to detect classes that can be sealed. The performance benefits of sealed classes are described in this issue.
##Calling virtual methods
When calling virtual methods, the runtime resolves the target method based on the object's actual type. Each type has a Virtual Method Table (vtable) that contains the addresses of all virtual methods. These pointers are used at runtime to invoke the appropriate method implementations (dynamic dispatch).
If the JIT knows the actual type of the object, it can skip the vtable and call the method directly, improving performance. Using sealed types helps the JIT because it guarantees no derived class can exist.
C#
public class SealedBenchmark
{
readonly NonSealedType nonSealedType = new();
readonly SealedType sealedType = new();
[Benchmark(Baseline = true)]
public void NonSealed()
{
// The JIT cannot know the actual type of nonSealedType. Indeed,
// it could have been set to a derived class by another method.
// So, it must use a virtual call to be safe.
nonSealedType.Method();
}
[Benchmark]
public void Sealed()
{
// The JIT is sure sealedType is a SealedType. As the class is sealed,
// it cannot be an instance from a derived type.
// So it can use a direct call which is faster.
sealedType.Method();
}
}
internal class BaseType
{
public virtual void Method() { }
}
internal class NonSealedType : BaseType
{
public override void Method() { }
}
internal sealed class SealedType : BaseType
{
public override void Method() { }
}
| Method | Mean | Error | StdDev | Median | Ratio | Code Size |
|---|
| NonSealed | 0.4465 ns | 0.0276 ns | 0.0258 ns | 0.4437 ns | 1.00 | 18 B |
| Sealed | 0.0107 ns | 0.0160 ns | 0.0150 ns | 0.0000 ns | 0.02 | 7 B |
Note that when the JIT can determine the actual type, it can use a direct call even if the type is not sealed. For instance, there is no difference between the following two snippets:
C#
void NonSealed()
{
var instance = new NonSealedType();
instance.Method(); // The JIT knows `instance` is NonSealedType because it is set
// in the method and never modified, so it uses a direct call
}
void Sealed()
{
var instance = new SealedType();
instance.Method(); // The JIT knows instance is SealedType, so it uses a direct call
}
##Casting objects (is / as)
When casting objects, the runtime must verify the object's type. For non-sealed types, it must walk the entire inheritance hierarchy. For sealed types, only a single type check is needed, making the operation faster.
C#
public class SealedBenchmark
{
readonly BaseType baseType = new();
[Benchmark(Baseline = true)]
public bool Is_Sealed() => baseType is SealedType;
[Benchmark]
public bool Is_NonSealed() => baseType is NonSealedType;
}
internal class BaseType {}
internal class NonSealedType : BaseType {}
internal sealed class SealedType : BaseType {}
| Method | Mean | Error | StdDev | Ratio |
|---|
| Is_NonSealed | 1.6560 ns | 0.0223 ns | 0.0208 ns | 1.00 |
| Is_Sealed | 0.1505 ns | 0.0221 ns | 0.0207 ns | 0.09 |
##Arrays
Arrays in .NET are covariant. This means that BaseType[] value = new DerivedType[1] is valid. This is not the case for generic collections – for instance, List<BaseType> value = new List<DerivedType>(); is not valid.
Array covariance comes with a performance cost: the JIT must verify the element type before assigning an item into an array. With sealed types, the JIT can skip this check. See Jon Skeet's post for more details on the performance penalties.
C#
public class SealedBenchmark
{
SealedType[] sealedTypeArray = new SealedType[100];
NonSealedType[] nonSealedTypeArray = new NonSealedType[100];
[Benchmark(Baseline = true)]
public void NonSealed()
{
nonSealedTypeArray[0] = new NonSealedType();
}
[Benchmark]
public void Sealed()
{
sealedTypeArray[0] = new SealedType();
}
}
internal class BaseType { }
internal class NonSealedType : BaseType { }
internal sealed class SealedType : BaseType { }
| Method | Mean | Error | StdDev | Ratio | Code Size |
|---|
| NonSealed | 3.420 ns | 0.0897 ns | 0.0881 ns | 1.00 | 44 B |
| Sealed | 2.951 ns | 0.0781 ns | 0.0802 ns | 0.86 | 58 B |
##Converting arrays to Span<T>
Arrays can be converted to Span<T> or ReadOnlySpan<T>. For the same reasons as the previous section, the JIT must verify the element type before performing the conversion. With a sealed type, this check is skipped, slightly improving performance.
C#
public class SealedBenchmark
{
SealedType[] sealedTypeArray = new SealedType[100];
NonSealedType[] nonSealedTypeArray = new NonSealedType[100];
[Benchmark(Baseline = true)]
public Span<NonSealedType> NonSealed() => nonSealedTypeArray;
[Benchmark]
public Span<SealedType> Sealed() => sealedTypeArray;
}
public class BaseType {}
public class NonSealedType : BaseType { }
public sealed class SealedType : BaseType { }
| Method | Mean | Error | StdDev | Ratio | Code Size |
|---|
| NonSealed | 0.0668 ns | 0.0156 ns | 0.0138 ns | 1.00 | 64 B |
| Sealed | 0.0307 ns | 0.0209 ns | 0.0185 ns | 0.50 | 35 B |
##Detecting unreachable code
With a sealed type, the compiler can detect invalid conversions and report warnings or errors, reducing bugs and eliminating unreachable code.
C#
class Sample
{
public void Foo(NonSealedType obj)
{
_ = obj as IMyInterface; // ok because a derived class can implement the interface
}
public void Foo(SealedType obj)
{
_ = obj is IMyInterface; // ⚠️ Warning CS0184
_ = obj as IMyInterface; // ❌ Error CS0039
}
}
public class NonSealedType { }
public sealed class SealedType { }
public interface IMyInterface { }
#Finding types that could be sealed
Meziantou.Analyzer contains a rule that checks for types that could be sealed.
Shell
dotnet add package Meziantou.Analyzer
It should report any internal class that could be sealed using MA0053:

You can also instruct the analyzer to report public classes by editing the .editorconfig file:
.editorconfig
[*.cs]
dotnet_diagnostic.MA0053.severity = suggestion
# Report public classes without inheritors (default: false)
MA0053.public_class_should_be_sealed = true
# Report class without inheritors even if there is virtual members (default: false)
MA0053.class_with_virtual_member_shoud_be_sealed = true
You can use a tool such as dotnet format to fix the solution:
Shell
dotnet format analyzers --severity info
Also, Roslyn analyzers can only analyze a single assembly at a time. To analyze all assemblies in a solution, you can use Roslyn to load the solution and analyze all projects. Here's an example: How to Find All Types That Can Be Sealed Using Roslyn
#Additional notes
All benchmarks were run using the following configuration:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22153.17
[Host] : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
#Additional resources
Do you have a question or a suggestion about this post? Contact me!