Blog
Why all your classes should be sealed by default in C#
I never understood why classes in C# weren't sealed by default when you create a new one. In every company I have worked for I'm known as the "seal your classes" and "make everything private or internal" guy, and I'm ok with that. A sealed class just seems to be the default behavior to me. By default, I consider that everything needs to be as private and as little inheritable as possible, because it just makes sense to me. Why expose objects that don't need to be exposed? Or why let people inherit from your classes if you don't need them to do so?
First of all, it's a question of common sense to me: close everything that is not required to be open, and do it immediately because if you do it later it could be a breaking change, if you maintain a package for example. It's much better to open a sealed class that needs to be inherited from one day than to do the opposite. Furthermore, inheritance is often considered an anti-pattern, and a lot of people and organizations prefer composition over inheritance.
But also, did you know that sealing the classes that can (and must) be sealed will have a positive effect on your application's performance?
Let's see that with a little benchmark.
Assume we have three basic classes: one base class (which is not abstract because we will instantiate it for the benchmark, although it probably would be in the real world), one open class and one sealed class, the last two inheriting from the base class:
public class BaseClass
{
public virtual void ExampleVoidMethod() { }
public virtual int ExampleIntMethod() => 0;
}
public class OpenClass : BaseClass
{
public override void ExampleVoidMethod() { }
public override int ExampleIntMethod() => 2022;
}
public sealed class SealedClass : BaseClass
{
public override void ExampleVoidMethod() { }
public override int ExampleIntMethod() => 450;
}
We now create a basic benchmark class that tests the two methods of the open and the sealed class. The code looks like this:
using BenchmarkDotNet.Attributes;
namespace SealedClassPerformanceLab;
[MemoryDiagnoser(false)]
public class Benchmarks
{
private readonly BaseClass baseClass = new();
private readonly OpenClass openClass = new();
private readonly SealedClass sealedClass = new();
[Benchmark]
public void OpenVoid() => openClass.ExampleVoidMethod();
[Benchmark]
public void SealedVoid() => sealedClass.ExampleVoidMethod();
[Benchmark]
public int OpenInt() => openClass.ExampleIntMethod() + 1500;
[Benchmark]
public int SealedInt() => sealedClass.ExampleIntMethod() + 1500;
}
Let's run this benchmark and take a look at the results:
| Method | Mean | Error | StdDev | Median | Allocated |
|----------- |----------:|----------:|----------:|----------:|----------:|
| OpenVoid | 0.7043 ns | 0.0313 ns | 0.0277 ns | 0.7014 ns | - |
| SealedVoid | 0.0003 ns | 0.0014 ns | 0.0014 ns | 0.0000 ns | - |
| OpenInt | 1.1048 ns | 0.0275 ns | 0.0244 ns | 1.1076 ns | - |
| SealedInt | 0.0074 ns | 0.0113 ns | 0.0106 ns | 0.0000 ns | - |
With an attached debugger.
| Method | Mean | Error | StdDev | Median | Allocated |
|----------- |----------:|----------:|----------:|----------:|----------:|
| OpenVoid | 0.4417 ns | 0.0129 ns | 0.0108 ns | 0.4408 ns | - |
| SealedVoid | 0.0083 ns | 0.0126 ns | 0.0098 ns | 0.0060 ns | - |
| OpenInt | 0.9771 ns | 0.0377 ns | 0.0353 ns | 0.9788 ns | - |
| SealedInt | 0.0180 ns | 0.0202 ns | 0.0224 ns | 0.0062 ns | - |
With no attached debugger.
As you can see, the methods of the sealed classes are faster to process. It is also the case if you check the type of a class, let's add some methods to measure it:
[Benchmark]
public bool IsCheckOpen() => baseClass is OpenClass;
[Benchmark]
public bool IsCheckSealed() => baseClass is SealedClass;
The results:
| Method | Mean | Error | StdDev | Allocated |
|-------------- |----------:|----------:|----------:|----------:|
| IsCheckOpen | 1.7381 ns | 0.0606 ns | 0.0789 ns | - |
| IsCheckSealed | 0.0776 ns | 0.0102 ns | 0.0095 ns | - |
As you can see, it's ten times faster, just by sealing it. This is an interesting issue about the performance gain of sealing classes: Analyzer Proposal: Seal internal/private types
Why not warn, or even throw a compilation error, when a class that could be sealed remains open? You can do this by modifying the editor configuration in Visual Studio. To do this, add a file named .editorconfig in your project, then update the value of the CA1852 rule:
Don't forget: don't open what doesn't need to be open!