Sealing a class means basically it is forbidden to inherit from that specific class. If you try anyway, you will get a compiler error. This is the main benefit, and you also get some performance improvements with it. This article will show you why plus you get my personal take on sealed
classes. One word of caution: The performance benefits are very, very small and, in almost all cases, neglectable. You could ask why I am explaining that stuff, then. Well, because I like this nerdy stuff a lot, and I'd like to know what is going on in the runtime when I do certain things.
virtual
functions
The reason one would open a class for inheritance is so that someone else can change the behavior of that class and can use it's "internal" state. To offer this we can use the virtual
function. If you are not familiar with virtual
functions I got you covered here.
Why should a sealed class be faster than a class with virtual functions? The answer is simple: We don't have to check which function we have to call. You see if we have something like this:
BaseClass baseClass = new DerivedClass();
baseClass.DoVirtualFunction();
The runtime has to check whether it has to call BaseClass.DoVirtualFunction
or DerivedClass.DoVirtualFunction()
and this costs time.
If you are interested how this looks in code you can go here to sharplab.io.
is
and as
These two keywords perform a check if we can cast a object to a specific type. Now why should checks against a type have a performance benefit if the type is sealed
.
Have a look at the following example:
public class MyType
{
}
MyType a = CreateMyType();
var isDisposable = a is IDisposable
Now pretty obvious that isDisposable
is always false
. Unfortunately no it is not. If we extend the example a bit we can see the following:
public class MyType { }
public class DerivedType : MyType, IDisposable {}
MyType a = CreateMyType();
var isDisposable = a is IDisposable
Then there is a chance that CreateMyType
returns DerivedType
which in fact is a IDisposable
. If we would have a sealed MyType
we can omit the runtime check because we know at compile-time that this comparison is false.
The performance implications are really small on this one:
public class VirtualFunctionBenchmark
{
private static readonly BaseClass _derivedClass = new DerivedClass();
private static readonly SealedClass _sealedClass = new();
[Benchmark]
public bool Is_NonSealed() => _derivedClass is IDisposable;
[Benchmark]
public bool Is_Sealed() => _sealedClass is IDisposable;
}
public class BaseClass { }
public class DerivedClass : BaseClass, IDisposable
{
public void Dispose() { }
}
public sealed class SealedClass { }
Results:
| Method | Mean | Error | StdDev | Median |
|------------- |----------:|----------:|----------:|----------:|
| Is_NonSealed | 0.0375 ns | 0.0259 ns | 0.0590 ns | 0.0061 ns |
| Is_Sealed | 0.0061 ns | 0.0092 ns | 0.0082 ns | 0.0024 ns |
The check is 5x faster when we handle with a sealed class. But be aware, even the "non-optimized" version is only 0.04 nanoseconds.
Now the central question: Should I seal them?
sealed
via default
Now this is my very own personal take and please take it with a grain of salt: I would prefer if classes are sealed
by default and you have to explicitily allow a class to be inherited from. Why do I say that? Because it gives performance improvements? Well no. Where "free" performance is nice that is not my main take. Getting a proper API design right is not so trivial. There are many aspects one has to consider when you "open the door" for everyone. I know I am a .NET guy, but still I have to quote from a Java book:
Design and document for inheritance or else prohibit it
And that makes obviously to me sense. There are some key features I do love about sealed
classes:
- As said, getting the design right is hard. You don't want to have a security class where key details can get overwritten and compromise the security. If you want to have a simpler example: Try to get Equals right with inheritance.
- A non-sealed class CAN'T guarantee immutability.
- Overuse of inheritance. I saw projects with 7 levels of inheritance and a lot of switch cases. Yes that seems counter-intuitive but it exists because it is hard to change the API later on.
- Plays nicely with Composition over inheritance
- Modifying the behavior or structure of your class can potentially break subclasses
But that is just my take on the topic.
Conclusion
If we use sealed
type the runtime can remove or simplify some checks for use with which it can give us better performance. But more important with the sealed
keyword we can express our intend.