В C# есть ключевое слово «sealed». Применяется оно в форме модификатора к классам, методам, свойствам и событиям, но сегодня речь пойдёт только про классы.
[1] Спецификация C# 4.0 (раздел «10.1.1.2 Sealed classes»).
[2] «Framework Design Guidelines», second edition (глава 8, раздел «8.2 Attributes»).
[3] «CLR via C#», third edition (глава 6, раздел «Using Type Visibility and Member Accessibility Intelligently»).
[4] «C# 4.0 Unleashed» (глава 14, раздел «Inheritance»).
Что все знают про sealed-классы
Применительно к классу, модификатор sealed означает, что от класса нельзя наследоваться. Это правило проверяется на этапе компиляции — если попытаться отнаследоваться от sealed-класса код просто не будет компилироваться. По умолчанию модификатора sealed у класса нет, т.е. любой класс изначально может быть использован в качестве базового (разумеется, за исключением static-классов).Нужны ли вообще sealed-классы?
На эту тему изломано немало копий. Как и в любом споре здесь есть две стороны. Первая сторона считает sealed-классы ненужными (и даже вредными). Основные аргументы такие:- В других языках прекрасно обходятся без sealed-классов, поэтому sealed-классы и в C# не нужны.
- От sealed-класса невозможно отнаследоваться в случае надобности.
- Модификатор sealed нужно специально прописывать, это лишняя работа с сомнительной пользой.
- Выполнять оптимизации.
- Выявлять ошибки и несогласованности.
Возможности для оптимизации
Sealed-классы дают не так уж много возможностей для оптимизации, я знаю только про две возможности. Первая — это оптимизация обращений к виртуальным членам класса (методам, свойствам, событиям). Знание того факта, что у класса нет наследников, позволяет вызывать его виртуальные члены как невиртуальные, а это несколько быстрее[1]. Подчеркну, что это всего лишь возможность. Никаких гарантий выполнения этой оптимизации никто не даёт. Второй вид оптимизаций — поиск атрибутов, являющихся sealed-классами, выполняется быстрее[2].Выявление ошибок и несогласованностей
Главный плюс sealed-классов для меня заключается в том, что они вносят в код новую семантику, которая предоставляет дополнительную информацию инструментам (компилятору, intellisense, ReSharper и т.п.). Это, в свою очередь, позволяет выявлять существующие ошибки и избыточный код, предотвращать появление новых ошибок.Модификаторы sealed, abstract и static
По сути, модификаторы sealed, abstract и static входят в одну группу. Они определяют, как можно использовать класс:- Если у класса нет модификаторов sealed, abstract и static, класс может использоваться как для создания экземпляров класса (инстанцирование), так и в качестве базового класса.
- Если у класса есть модификатор abstract, то он может использоваться только в качестве базового.
- Если у класса есть модификатор sealed, он может использоваться только для создания экземпляров.
- Модификатор static запрещает создавать экземпляры класса и использовать класс в качестве базового. Фактически, модификатор static — это сокращение для abstract sealed.
- Если нужен статический класс — создаём обычный класс, в котором будут только статические члены, никогда не создаём экземпляров этого класса и никогда от него не наследуемся.
- Если нужен sealed-класс — создаём обычный класс и никогда от него не наследуемся.
- Если нужен абстрактный класс — создаём обычный класс и никогда не создаём его экземпляров.
- Это делается только один раз в одном месте (в том числе и для partial-классов), единообразным способом.
- В случае соглашений всё равно придётся искать замену модификаторам и как-то и где-то указывать на нужное соглашение (например, в комментарии: «Этот класс статический, не создавайте его экземпляров»).
Какие ошибки помогают выявить sealed-классы?
То, что компилятор не будет компилировать код, в котором есть наследование от sealed-класса, понятно, в этом и есть суть sealed-классов. Но какие ещё ошибки можно выявить, если использовать модификатор sealed? Рассмотрим небольшой пример:using System.Linq; namespace Sample { public class Foo {} public class GenericClass<T> where T : Foo {} public interface IBar {} internal static class Program { private static void SomeMethod(Foo foo, IBar bar) { var x1 = (IBar)foo; var x2 = (Foo)bar; var x3 = foo as IBar; var x4 = bar as Foo; if (foo is IBar) {} if (bar is Foo) {} if (foo == bar) {} if (foo != bar) {} foreach (Foo x in Enumerable.Empty<IBar>()) {} foreach (IBar x in Enumerable.Empty<Foo>()) {} var x5 = (IBar[])new Foo[0]; var x6 = (Foo[])new IBar[0]; } private static void Main() {} } }Этот код успешно компилируется, без ошибок и предупреждений. Теперь давайте введём условие: «Класс Foo не имеет наследников» и проанализируем код с учётом этого условия:
- public class GenericClass<T> where T : Foo {} Здесь generic-параметр T фактически может быть только одного типа — Foo, т.к. у Foo нет наследников. Это значит, что generic-параметр в этом классе вообще не нужен, вместо T везде можно подставить Foo.
- Оба приведения типов будут в процессе выполнения гарантировано приводить к генерации исключения InvalidCastException, т.к. ни класс Foo, ни его наследники (которых просто нет) не реализуют интерфейс IBar.
- Оба оператора as всегда будут возвращать только null (см. п. 2).
- Оба оператора is всегда будут возвращать false (см. п. 2).
- Оператор равенства всегда будет возвращать false, оператор неравенства — true (если только оба сравниваемых значения не равны null) (см. п. 2).
- При выполнении циклов будет генерироваться исключение из-за приведения типов, которое сгенерирует компилятор (см. п. 2).
- При приведении типов массивов так же будет сгенерировано исключение (см. п. 2).
public sealed class Foo {}После этого шесть пунктов из перечисленных семи будут приводить к ошибкам времени компиляции. Пункт 4 будет приводить к предупреждениям (warning) компилятора. Что же происходит при добавлении модификатора sealed? Для этого стоит обратиться к спецификации языка (я буду ссылаться на спецификацию C# 4.0):
- В разделе «10.1.5 Type parameter constraints» говорится о том, что sealed-класс не может использоваться в качестве ограничения на тип generic-параметра.
- В разделе «6.2.4 Explicit reference conversions» перечисляются требования к типам, участвующим в приведении. Там говорится, что при приведении от типа класса к типу интерфейсу тип класса не должен реализовывать интерфейс, от которого идёт приведение и не быть sealed-классом; при приведении от типа интерфейса к типу класса тип класса должен или реализовывать интерфейс, к которому происходит приведение, или не быть sealed-классом.
Когда использовать модификатор sealed?
Ответ на этот вопрос зависит от того, какой код пишется. Если это какой-то фреймворк или библиотека или внешний API для сторонних разработчиков, очень трудно заранее предугадать, как будут использоваться публичные классы. Применение атрибута sealed ко всем классам без разбора может помешать пользователям вашей библиотеки использовать её так, как они считают нужным. Однако следует помнить, что всегда можно убрать модификатор sealed в новой версии библиотеки, а вот добавить его, из-за риска огрести проблемы с совместимостью, можно не всегда. Кроме того, во многих случаях есть другие альтернативы наследованию:- Наследник нужен для добавления дополнительных методов/свойств — можно использовать extension-методы.
- Наследник нужен для упрощения конструирования объекта, чтобы передавать в конструктор базового класса предопределённые значения параметров — можно использовать фабричные методы.
- Наследник нужен для реализации интерфейса — можно использовать делегирование, создать класс-обёртку, который реализует интерфейс и перенаправляет вызовы объекту, выполняющему работу.
- Наследник нужен чтобы каждый раз не задавать тип generic-параметра (class ListOfStrings : List<string> {}) — можно создать псевдоним с помощью директивы using (using ListOfStrings = System.Collections.Generic.List<string>;).
[1] Спецификация C# 4.0 (раздел «10.1.1.2 Sealed classes»).
[2] «Framework Design Guidelines», second edition (глава 8, раздел «8.2 Attributes»).
[3] «CLR via C#», third edition (глава 6, раздел «Using Type Visibility and Member Accessibility Intelligently»).
[4] «C# 4.0 Unleashed» (глава 14, раздел «Inheritance»).
Отличное, всестороннее описание.
ОтветитьУдалитьТеперь есть одна ссылка которую можно дать на ревью :).
А я то думал зачем может sealed понадобится, кроме как для повышения быстродействия. Спасибо, теперь буду знать.
ОтветитьУдалитьСпасибо, оч познавательная статья
ОтветитьУдалитьмне нравится как обстоятельно вы все по пунктам расписываете.
Спасибо за статью!
ОтветитьУдалить