Sealed Classes in C#: Locking Down for Safety & Speed

Myth vs Reality

Myth: Sealed classes are just a performance trick for micro-optimizations that don't matter in real code. Reality: Sealed classes communicate design intent, prevent unintended inheritance, enable JIT compiler optimizations, and protect class invariants. The performance benefit is secondary to design clarity.

The sealed keyword prevents a class from being inherited or a method from being overridden in derived classes. When you mark a class sealed, you tell the compiler and other developers that this type is complete and not designed for extension through inheritance. This enables devirtualization, inlining, and other optimizations while making your design intentions explicit.

You'll learn when sealing classes improves both design and performance, how to seal specific methods while allowing class inheritance, and why modern .NET libraries increasingly use sealed by default. You'll see concrete examples of JIT optimizations and understand the trade-offs between extensibility and control.

Understanding Sealed Classes

A sealed class cannot be used as a base class. Any attempt to inherit from it causes a compilation error. This makes sealed classes final in the inheritance hierarchy—they're leaf nodes with no descendants. The compiler and JIT know that methods on sealed classes will never be overridden, enabling optimizations.

Sealing classes is common for types that represent concrete implementations rather than abstractions. Data transfer objects, specific service implementations, and utility classes are good candidates. If you didn't design a class specifically to be inherited, sealing it prevents accidental misuse.

SealedBasics.cs - Basic sealed class
namespace SealedDemo;

// Sealed class cannot be inherited
public sealed class UserSettings
{
    public string Theme { get; set; } = "Light";
    public int FontSize { get; set; } = 12;
    public bool Notifications { get; set; } = true;

    public void ApplyDefaults()
    {
        Theme = "Light";
        FontSize = 12;
        Notifications = true;
    }

    public UserSettings Clone()
    {
        return new UserSettings
        {
            Theme = this.Theme,
            FontSize = this.FontSize,
            Notifications = this.Notifications
        };
    }
}

// This would cause a compilation error:
// public class CustomSettings : UserSettings { }
// Error: cannot derive from sealed type 'UserSettings'

// Sealed works with records too
public sealed record LoginRequest(string Username, string Password);

// Usage
var settings = new UserSettings { Theme = "Dark", FontSize = 14 };
var clone = settings.Clone();

The UserSettings class is sealed because it's a concrete configuration type not designed for inheritance. Allowing inheritance could break the Clone method or ApplyDefaults logic if derived classes add fields. Sealing prevents these issues by making inheritance impossible.

Records often benefit from sealing too. A LoginRequest has specific validation rules and shouldn't be extended with additional properties that bypass those rules. Sealing communicates this restriction clearly.

Sealing Override Methods

You can seal individual overridden methods without sealing the entire class. This prevents further derived classes from overriding that specific method while allowing them to override others. Use sealed override together to lock down critical behavior in inheritance hierarchies.

This pattern appears when you inherit from a base class, override a method to provide specific behavior, and want to prevent subclasses from changing that behavior again. It's useful for securing algorithms or enforcing invariants at a specific level of the hierarchy.

SealedMethods.cs - Sealing specific methods
namespace SealedDemo;

public abstract class DataProcessor
{
    public virtual void ValidateData(string data)
    {
        if (string.IsNullOrEmpty(data))
            throw new ArgumentException("Data cannot be empty");
    }

    public abstract void ProcessData(string data);
}

public class SecureDataProcessor : DataProcessor
{
    // Seal the validation method to prevent weakening security
    public sealed override void ValidateData(string data)
    {
        base.ValidateData(data);

        // Additional security checks that must not be bypassed
        if (data.Contains("<script>"))
            throw new ArgumentException("Potential XSS detected");

        if (data.Length > 1000)
            throw new ArgumentException("Data too large");
    }

    public override void ProcessData(string data)
    {
        ValidateData(data);
        Console.WriteLine($"Processing: {data}");
    }
}

// This class can inherit from SecureDataProcessor
public class LoggingDataProcessor : SecureDataProcessor
{
    // Can override ProcessData
    public override void ProcessData(string data)
    {
        Console.WriteLine($"[LOG] Starting processing");
        base.ProcessData(data);
        Console.WriteLine($"[LOG] Completed processing");
    }

    // Cannot override ValidateData - it's sealed
    // public override void ValidateData(string data) { } // Error!
}

The ValidateData method is sealed in SecureDataProcessor to prevent derived classes from weakening security checks. The XSS and length validations must always run, so sealing prevents overrides that might skip them. Meanwhile, ProcessData remains virtual for legitimate customization.

This pattern balances extensibility with safety. Derived classes can extend functionality through ProcessData but cannot compromise the security model by overriding validation. It's a precise tool for controlling inheritance at the method level.

JIT Compiler Optimizations

The JIT compiler can optimize sealed classes more aggressively than non-sealed ones. When the JIT sees a method call on a sealed class, it knows the exact implementation—there's no possibility of a derived class overriding it. This enables devirtualization, turning virtual method calls into direct calls, and then inlining the method body.

For virtual methods on non-sealed classes, the JIT must emit code that looks up the actual implementation at runtime through the virtual method table. This indirection prevents inlining and adds a small overhead. Sealing removes this overhead in hot code paths.

JitOptimization.cs - Devirtualization example
namespace SealedDemo;

// Non-sealed with virtual method
public class RegularCalculator
{
    public virtual int Add(int a, int b)
    {
        return a + b;
    }
}

// Sealed class - JIT can devirtualize and inline
public sealed class FastCalculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

// Demonstration of usage
public class PerformanceTest
{
    public static void UseRegular()
    {
        var calc = new RegularCalculator();

        int sum = 0;
        // Virtual call - JIT cannot inline easily
        for (int i = 0; i < 1000; i++)
        {
            sum += calc.Add(i, i);
        }
    }

    public static void UseSealed()
    {
        var calc = new FastCalculator();

        int sum = 0;
        // Direct call - JIT can inline to: sum += i + i
        for (int i = 0; i < 1000; i++)
        {
            sum += calc.Add(i, i);
        }
    }
}

In the UseSealed method, the JIT can recognize that FastCalculator.Add will never be overridden. It inlines the addition directly into the loop, eliminating method call overhead entirely. The loop body becomes just sum += i + i in the generated machine code.

For UseRegular, the JIT must assume that calc could actually point to a derived type at runtime, even though it doesn't in this specific code. This prevents inlining and requires a virtual dispatch on each call. The difference is measurable in tight loops but negligible in typical business logic.

Try It Yourself — Sealed Demo

Build a small program that demonstrates sealed classes and methods in action. This example shows how sealing prevents unintended inheritance while allowing controlled extension points.

Steps

  1. Create the project: dotnet new console -n SealedExample
  2. Move into directory: cd SealedExample
  3. Replace Program.cs with the code shown below
  4. Check SealedExample.csproj matches the config
  5. Execute: dotnet run
Program.cs
using System;

// Abstract base
abstract class Shape
{
    public abstract double GetArea();
}

// Sealed implementation
sealed class Circle : Shape
{
    public double Radius { get; init; }

    public override double GetArea() => Math.PI * Radius * Radius;
}

// Non-sealed with sealed method
class Rectangle : Shape
{
    public double Width { get; init; }
    public double Height { get; init; }

    public sealed override double GetArea() => Width * Height;
}

// Can inherit from Rectangle
class ColoredRectangle : Rectangle
{
    public string Color { get; init; } = "Red";

    // Cannot override GetArea - it's sealed
    // Can add new behavior instead
    public void PrintInfo() =>
        Console.WriteLine($"{Color} rectangle: {GetArea()} sq units");
}

// Usage
var circle = new Circle { Radius = 5 };
Console.WriteLine($"Circle area: {circle.GetArea():F2}");

var rect = new ColoredRectangle { Width = 4, Height = 3, Color = "Blue" };
rect.PrintInfo();

// Cannot do: class MyCircle : Circle { } // Error!
SealedExample.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Run result

Circle area: 78.54
Blue rectangle: 12 sq units

The Circle class is sealed because its area calculation is final—there's no valid way to derive and change it. Rectangle seals its GetArea method but allows inheritance for other purposes, like adding color information. This demonstrates selective sealing at the method level.

When Not to Use Sealed

Sealing classes has clear benefits, but it also closes doors permanently. Understanding when not to seal is as important as knowing when to seal. Wrong sealing decisions limit future flexibility and can force breaking changes later.

Don't seal library public types by default: Library consumers may need to inherit from your types for mocking, testing, or extending behavior in ways you didn't anticipate. Sealing public API types removes these options. Reserve sealing for types where inheritance would genuinely break invariants or where you're certain extension makes no sense. Internal types are safer to seal since you control all usage.

Avoid sealing abstract base classes: Abstract classes exist to be inherited. Sealing them contradicts their purpose. If you have an abstract class that you don't want further derived, reconsider whether it should be abstract at all. Perhaps it should be a concrete sealed class or a set of interfaces instead.

Don't seal for performance without measuring: The JIT does optimize sealed classes, but the benefit is usually small—single-digit percentage gains in tight loops at best. For typical business logic, the difference is unmeasurable. Seal for design reasons first, and treat performance as a bonus. Only seal specifically for performance if profiling shows virtual call overhead matters in your hot path.

Think twice about sealing framework integration points: If your class integrates with frameworks that use inheritance for extensibility—like ASP.NET Core controllers, Entity Framework entities, or WPF ViewModels—sealing may interfere with framework features. Check framework documentation before sealing types that frameworks might expect to derive from.

FAQ

Do sealed classes actually improve performance?

Yes, but the gain is typically small. The JIT can devirtualize method calls, inline code, and skip type checks. For hot paths with virtual methods, this helps. For most code, the design benefits outweigh micro-optimizations. Measure with BenchmarkDotNet to verify impact.

Can I seal individual methods instead of the whole class?

Yes. Use sealed override to prevent further overriding in derived classes. This locks down specific behavior while allowing other methods to remain virtual. The syntax is: public sealed override void MethodName(). Useful for securing critical logic in inheritance hierarchies.

Should I make all my classes sealed by default?

No. Seal classes that aren't designed for inheritance or where inheritance would break invariants. For library code, sealing prevents extensibility. For internal models and DTOs, sealing is fine. Design for inheritance explicitly or prohibit it explicitly—avoid the middle ground.

Back to Articles