вторник, 19 мая 2026 г.

Поговорим немного о sealed

    На прошлой работе мы с коллегами спорили о необходимости использования ключевого слова sealed для закрытия/запечатывания классов.


     Многие разработчики в проектах никогда не задумываются и продолжают делать что-то типа такого:

public static partial class FooUseCase
{
   public record Command(long ExecutionId) : IUseCaseCommand<Result>;
}
 
// ...
 
public static partial class FooUseCase
{
   public class Handler : IUseCaseCommandHandler<Command>
   { }
}

     Модификатор sealed уже давно существует в языке C#, но в современных версиях платформы .NET он получил дополнительный вес за счёт оптимизаций JIT.

     Краткая справка
     sealed запрещает наследование. Если класс помечен как sealed, от него нельзя будет создать дочерний класс. Кстати, можно применять и к отдельным методам с override. Тогда этот метод нельзя будет дальше переопределить по цепочке.

public sealed class AccountService
{
   private readonly IDbConnection _connection;  
}
 
// Ошибка компиляции: нельзя наследоваться от sealed-типа
public class DirectAccountService : AccountService
{ }

     И еще небольшая сноска: чем лично мне нравится простота и лаконичность языка Kotlin - это то, что по дефолту там классы запечатаны. И если нужно открыть класс к наследованию, то нужно добавить ключевое слово open .

     А теперь о самом интересном: почему это влияет на производительность?

     Когда класс открыт для наследования, JIT не может точно знать, какой именно метод будет вызван в рантайме. Поэтому он генерит косвенный вызов через vtable

; Открытый класс — несколько обращений к памяти перед вызовом
mov eax, [edx]
mov eax, [eax+0x28]
call dword ptr [eax+0x10]

     А вот уже для sealed-класса JIT точно знает, какой метод будет вызван. Он убирает обращение к vtable, встраивает тело метода прямо в место вызова и оставляет минимум сгенерированных инструкций:

; Sealed-класс — только проверка на null и возврат
cmp [edx], dl
ret

     Дополнительно ускоряются (из-за дальнейшей ненадобности прохождения по всему дереву наследования):
1) операторы is и as
2) поиск референсов на класс в IDE

     Но есть и кейсы, когда ключевое слово sealed перестает быть волшебной палочкой по улучшению производительности:
1) если вы юзаете ORM-ку, которая не сможет расширить прокси-классы в рантайме от сущности;
2) аналогичная проблема случится в либах при мокировании. Особенно если будете пробовать мокать не интерфейсы, а сами sealed-классы (чего делать крайне не рекомендую).

     Многие архитекторы и эксперты по производительности рекомендуют такой подход. В официальной документации Microsoft также есть анализатор, который советует запечатывать классы. В сообществе github ведутся дискуссии о добавлении в язык C# возможности запечатывания классов по умолчанию (sealed-by-default), многие участники высказываются в поддержку этой идеи. Также у Microsoft есть правило анализа кода CA1852: Seal internal types.

     Считаю, что в 99% случаев модификатор sealed защищает дизайн от случайных расширений и даёт прирост производительности без дополнительных вложений.

Ссылки на источники:

Комментариев нет:

Отправить комментарий