Why all your classes should be sealed by default in C#

I never understood why the classes in C# weren't sealed by default when you create a new one. In every company I worked for I'm know as the "seal your classe" 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 the more private and the less inheritable as possible, because it just make sense to me. Why exposing objects that they don't need to? Or why letting people inherits from your classes if you don't need them to do so?

Firstable, it's a question of common sense to me, close everything that does not required to be open, and do it immediatly because if you do it later that could be a breaking change, if you maintain a package for example. It's a way better to open a sealed class that needs to be inherithed some days that doing the opposite. Furthermore, inheritance is often considered as an anti-pattern, a lot of people and organizations prefers composition over inheritance.

But also, do you know that sealing the classes that can (must) be 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 instanciate it for the benchmark it probably would in the real world), one open class and one sealed class, the two last one are 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 create now 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 time faster, just as sealing it. This is an interesting issue about the performance gain of sealing the classes: Analyzer Proposal: Seal internal/private types

Why no warm or even throw a compilation exception when a class that could be sealed remains open? You can do it by modifying the configuration of the editor 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 does not need to be!

December 1, 2022
  • .NET