Что такое статический конструктор
Статический конструктор
Дата изменения: 15.09.2017
Статический конструктор – это специальный метод статического или нестатического класса. Необходим для инициализации статических полей. Также используется для вызова статических методов и однократного выполнения инструкций.
Программист не может вызвать статический конструктор, его вызывает среда выполнения (CLR) автоматически в двух случаях:
обращение к статическому члену;
создание экземпляра нестатического типа со статическими членами.
Программист может лишь изменить реализацию статического конструктора, определив его в области действия типа.
Объявление и использование
Статический конструктор объявляется с помощью ключевого слова static перед именем:
Использование статического конструктора полностью ложиться на виртуальные плечи среды выполнения:
Ограничения в определении и использовании
Статический конструктор не имеет параметров и при его объявлении нельзя использовать модификаторы доступа:
Статический конструктор нельзя перегрузить, поскольку сигнатура конструктора всегда одна;
Не участвует в наследовании. Поскольку статические поля не наследуются и в производном классе они могут быть другими, следовательно, статический конструктор определяется всякий раз, когда в классе есть статические поля.
Среда выполнения вызывает статический конструктор только один раз. Не важно сколько было создано объектов класса, в котором он был определен. Если первый вызов такого конструктора прошел неудачно (вызвал исключение), то второго раза не будет и статические поля останутся неинициализированными.
Применение
Статические конструкторы часто используются для записи в файл пользовательской или системной информации, при запуске программы или определенного блока кода. С помощью них, например, реализованы создание системными утилитами Log-файлов или файлов диагностики в Windows.
Удобно использовать статические конструкторы для погрузки динамических библиотек (dll). То есть использовать системный метод LoadLibrary с параметрами загрузки.
Ключевое слово static
Иногда требуется определить такой член класса, который будет использоваться независимо от всех остальных объектов этого класса. Как правило, доступ к члену класса организуется посредством объекта этого класса, но в то же время можно создать член класса для самостоятельного применения без ссылки на конкретный экземпляр объекта. Для того чтобы создать такой член класса, достаточно указать в самом начале его объявления ключевое слово static.
Если член класса объявляется как static, то он становится доступным до создания любых объектов своего класса и без ссылки на какой-нибудь объект. С помощью ключевого слова static можно объявлять как переменные, так и методы. Наиболее характерным примером члена типа static служит метод Main(), который объявляется таковым потому, что он должен вызываться операционной системой в самом начале выполняемой программы.
Для того чтобы воспользоваться членом типа static за пределами класса, достаточно указать имя этого класса с оператором-точкой. Но создавать объект для этого не нужно. В действительности член типа static оказывается доступным не по ссылке на объект, а по имени своего класса.
Переменные, объявляемые как static, по существу, являются глобальными. Когда же объекты объявляются в своем классе, то копия переменной типа static не создается. Вместо этого все экземпляры класса совместно пользуются одной и той же переменной типа static. Такая переменная инициализируется перед ее применением в классе.
Пример использования ключевого слова static:
На применение методов типа static накладывается ряд следующих ограничений:
В методе типа static должна отсутствовать ссылка this, поскольку такой метод не выполняется относительно какого-либо объекта
В методе типа static допускается непосредственный вызов только других методов типа static, но не метода экземпляра из того самого же класса. Дело в том, что методы экземпляра оперируют конкретными объектами, а метод типа static не вызывается для объекта. Следовательно, у такого метода отсутствуют объекты, которыми он мог бы оперировать
Аналогичные ограничения накладываются на данные типа static. Для метода типа static непосредственно доступными оказываются только другие данные типа static, определенные в его классе. Он, в частности, не может оперировать переменной экземпляра своего класса, поскольку у него отсутствуют объекты, которыми он мог бы оперировать
Статические конструкторы
Конструктор можно также объявить как static. Статический конструктор, как правило, используется для инициализации компонентов, применяемых ко всему классу, а не к отдельному экземпляру объекта этого класса. Поэтому члены класса инициализируются статическим конструктором до создания каких-либо объектов этого класса:
Обратите внимание на то, что конструктор типа static вызывается автоматически, когда класс загружается впервые, причем до конструктора экземпляра. Из этого можно сделать более общий вывод: статический конструктор должен выполняться до любого конструктора экземпляра. Более того, у статических конструкторов отсутствуют модификаторы доступа — они пользуются доступом по умолчанию, а следовательно, их нельзя вызывать из программы.
Статические классы
Класс можно объявлять как static. Статический класс обладает двумя основными свойствами. Во-первых, объекты статического класса создавать нельзя. И во-вторых, статический класс должен содержать только статические члены. Статический класс создается по приведенной ниже форме объявления класса, видоизмененной с помощью ключевого слова static.
Статические классы применяются главным образом в двух случаях. Во-первых, статический класс требуется при создании метода расширения. Методы расширения связаны в основном с языком LINQ. И во-вторых, статический класс служит для хранения совокупности связанных друг с другом статических методов:
Стоит отметить, что для статического класса не допускается наличие конструктора экземпляра, но у него может быть статический конструктор.
Статика в C#
Волею судьбы в последние годы у меня появилось ещё одно очень увлекательное хобби – учить. Я занимаюсь обучением людей, которые хотят стать программистами C#. Люди приходят разные: технари, гуманитарии, кто-то по своей воле, кого-то направляют от организаций. Не смотря на различные уровни, мне нужно их обучать. Поэтому я стараюсь постоянно обновлять и улучшать свои обучающие материалы. В связи с чем, пришёл к выводу: «А не плохо было бы оформить материалы в текстовом виде, чтобы ими было удобно пользоваться». Под катом я выложил как пример одну из недавно оформленных лекций.
Общая концепция
Статика подразумевает, что вам не нужно создавать экземпляр класса. Все приведённые выше составляющие класса, доступны посредством указания его имени.
Следует отметить, что необязательно делать весь класс статическим. Иногда достаточно применить статику для отдельных его членов.
Больше деталей
Выше мы не рассматривали такую конструкцию, как статический конструктор. Один из достаточно интересных вопросов, на мой взгляд, когда происходит вызов статического конструктор у классов?
Я думаю вы уже обратили внимание, что для статического конструктора не используется спецификатор доступа. Всё очень очевидно, создание статики вы не контролируете. Если попробовать выполнить приведённый ниже код, то можно убедиться в верности следующего утверждения: статический конструктор вызывается перед доступом к любому члену класса.
Можно поиграться, закомментировав любую из строк в которых происходит обращение к классу Box. Теперь немного изменим код, и подправим наше утверждение
В данном случае вызов статического конструктора не происходит. Итак: статический конструктор вызывается перед доступом к любому члену класса, за исключением констант. Я не зря использовал слово класс в данном определении. Со структурами очень много “приколов”. Вы должно быть знаете, что в C# нельзя переопределить конструктор по-умолчанию, но можно определить статический конструктор без параметров. Однако он будет вызываться не всегда, так например не произойдёт его вызов, если вы, например, попытаетесь создать массив структур.
Общие рассуждения об использовании статики
Полиморфизм
Как упоминалось выше, статические классы не поддерживают наследование, т.е. вы не можете наследоваться от интерфейса или другого класса и таким образом расширить функциональность.
Тестирование
При использование статики тестирование достаточно затруднено. Нельзя оперативно подменять код, основываясь на интерфейсах. Если нужно менять, то серьёзно, переписывая значительные куски кода.
Статические конструкторы (Руководство по программированию в C#)
Статический конструктор используется для инициализации любых статических данных или для выполнения определенного действия, которое требуется выполнить только один раз. Он вызывается автоматически перед созданием первого экземпляра или ссылкой на какие-либо статические члены.
Примечания
Статические конструкторы обладают следующими свойствами.
Статический конструктор не принимает модификаторы доступа и не имеет параметров.
Класс или структура могут иметь только один статический конструктор.
Статические конструкторы не могут быть унаследованы или перегружены.
Статический конструктор нельзя вызывать напрямую. Он предназначен только для вызова из общеязыковой среды выполнения (CLR). Он запускается автоматически.
Пользователь не управляет временем, в течение которого статический конструктор выполняется в программе.
Статический конструктор вызывается автоматически. Он инициализирует класс перед созданием первого экземпляра или ссылок на какие-либо статические элементы. Статический конструктор выполняется раньше, чем конструктор экземпляра. Статический конструктор типа вызывается при вызове статического метода, назначенного делегату или событию, а не при его назначении. Если в классе со статическим конструктором присутствуют инициализаторы переменных для статических полей, они выполняются в том порядке, в котором они появляются в объявлении класса. Эти инициализаторы выполняются непосредственно перед выполнением статического конструктора.
Если вы не предоставили статический конструктор для инициализации статических полей, все статические поля инициализируются значениями по умолчанию, которые указаны в разделе Значения по умолчанию типов C#.
Если статический конструктор создает исключение, среда выполнения не вызывает его во второй раз, и тогда тип остается неинициализированным на все время существования домена приложения. Чаще всего исключение TypeInitializationException вызывается, когда статический конструктор не может создать экземпляр типа или в статическом конструкторе создается необработанное исключение. Для устранения неполадок со статическими конструкторами, которые не определены явным образом в исходном коде, может потребоваться проверка кода на промежуточном языке (IL).
Наличие статического конструктора не позволяет добавлять атрибут типа BeforeFieldInit. Это ограничивает возможности оптимизации во время выполнения.
Среда выполнения вызывает статический конструктор не более одного раза для каждого домена приложения. Этот вызов выполняется в заблокированном регионе на основе определенного типа класса. В тексте статического конструктора не нужны дополнительные механизмы блокировки. Чтобы избежать риска взаимоблокировки, не блокируйте текущий поток в статических конструкторах и инициализаторах. Например, не следует ожидать выполнения задач, потоков, обработчиков ожидания или событий, а также создавать блокировки или выполнять блокирующие параллельные операции, как, например, параллельные циклы, Parallel.Invoke и запросы Parallel LINQ.
Наличие явного статического конструктора следует документировать, несмотря на отсутствие прямого доступа к нему, поскольку это важно для устранения неполадок с исключениями при инициализации.
Использование
Типичным использованием статических конструкторов является случай, когда класс использует файл журнала и конструктор применяется для добавления записей в этот файл.
Кроме того, статические конструкторы очень удобны для проведения проверок времени выполнения для параметров типов, которые нельзя проверить во время компиляции с помощью ограничений параметров типов.
Пример
О синглтонах и статических конструкторах
Давайте рассмотрим одну из наиболее простых и популярных реализаций паттерна Синглтон (*), основанную на инициализаторе статического поля:
public sealed class Singleton
<
private static readonly Singleton instance = new Singleton();
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Singleton()
<>
Потокобезопасность этой реализации основывается на том, что статический конструктор (или, другими словами, инициализатор типа) в одном домене гарантировано вызывается не более одного раза. А раз так, то со стороны разработчика не нужно делать никакие финты ушами ради того, чтобы сделать потокобезопасность еще более безопасной. Большинство разработчиков (и до недавнего времени и я в том числе) на этом успокаиваются, поскольку основную проблему, которая может произойти с любым синглтоном, мы уже решили, так что на комментарий пустого статического конструктора мало кто обращает внимание. А поскольку этот комментарий вообще нифига не понятен, то пустой статический конструктор просто не доходит до многих реализаций в реальных приложениях.
Статический конструктор и инициализаторы полей
Итак, давайте рассмотрим следующий код:
public static string S = Echo( «Field initializer» );
Как видно, что статическое поле будет проинициализировано, хотя сам тип в приложении не используется. Практика показывает, что в большинстве случаев при отсутствии явного конструктора, JIT-компилятор вызывает инициализатор статических переменных непосредственно перед вызовом метода, в котором используется эта переменная. Если раскомментировать статический конструктор класса Singleton, то поведение будет именно таким, которое ожидает большинство разработчиков – инициализатор поля вызван не будет и при запуске приложения на экране будет только одна строка: “Starting Main…”.
Статические конструкторы и взаимоблокировка
Поскольку статический конструктор указанного типа должен вызываться не более одного раза в домене приложения, то CLR вызывает его внутри некоторой блокировки. Тогда, если поток, исполняющий статический конструктор будет ожидать завершения другого потока, который в свою очередь попытается захватить ту же самую внутреннюю блокировку CLR, мы получим классический дедлок.
Воспроизвести подобную ситуацию в идеальных условиях весьма просто: для этого в статическом конструкторе достаточно создать новый поток и попытаться дождаться его выполнения:
class Program
<
static Program()
<
var thread = new Thread(o => < >);
thread.Start();
thread.Join();
>
Причем, если обратиться к спецификации CLI, то в ней сказано, что дедлок возможен при явном или не явном вызове блокирующей операции внутри статического конструктора. На практике это означает, что попытка блокировки потока внутри статического конструктора, например, из-за использования именованного мьютекса в некоторых случаях может приводить к дедлоку.
Бага в реальном приложении
Вся эта чепуха, связанная со временем вызова инициализаторов полей и дедлоками в статических конструкторах, может показаться высосанной из пальца и маловероятной в реальных приложениях. Если честно, я был такого же мнения еще неделю назад, до того, как провел целый день в отладке одного весьма неприятного бага.
Итак, вот симптомы реальной проблемы, с которой я столкнулся. У нас есть сервис, который прекрасно работает в консольном режиме, а также не менее прекрасно работает в виде сервиса, если собрать его в Debug-е. Однако если собрать его в релизе, то он запускается через раз: один раз запускается успешно, а во второй раз запуск падает по тайм-ауту (по умолчанию SCM прибивает процесс, если сервис не запустился за 30 секунд).
В результате отладки было найдено следующее. (1) У нас есть класс сервиса, в конструкторе которого происходит создание счетчиков производительности; (2) класс сервиса реализован в виде синглтона с помощью инициализации статического поля без явного статического конструктора, и (3) этот синглтон использовался напрямую в методе Main для запуска сервиса в консольном режиме:
// Класс сервиса
partial class Service : ServiceBase
<
// «Кривоватая» реализаци Синглтона. Нет статического конструктора
public static readonly Service instance = new Service();
public static Service Instance < get < return instance; >>
public Service()
<
InitializeComponent();
// В конструкторе инициализирутся счетчики производительности
var counters = new CounterCreationDataCollection();
if (PerformanceCounterCategory.Exists(category))
PerformanceCounterCategory.Delete(category);
PerformanceCounterCategory.Create(category, description,
PerformanceCounterCategoryType.SingleInstance, counters);
>
// Метод запуска сервиса
public void Start()
<>
const string category = «Category» ;
const string description = «Category description» ;
>
// А тем временем в классе Program
static void Main( string [] args)
<
if (args[0] == «—console» )
Service.Instance.Start();
else
ServiceBase.Run( new Service());
Поскольку класс Server не содержит явного статического конструктора и компилятор C# добавляет флаг beforeFieldInit, то вызов конструктора класса Service происходит до вызова метода Main. При этом для создания категории счетчиков производительности используется именованный мьютекс, что в определенных условиях приводит к дедлоку приложения: во время первого запуска указанной категории еще нет в системе, поэтому метод Exists возвращает false и метод Create завершается успешно; во время следующего запуска метод Exists возвращает true, метод Delete завершается успешно, но метод Create подвисает на веки. Понятное дело, что после того, как проблема была найдена, решение заняло ровно 13 секунд: добавить статический конструктор в класс Service.
Заключение
Пример с багом в реальном приложении говорит о том, что статьи о подводных камнях языка C# и о правильном применении известных паттернов и идиом не является бредом и выдумкой теоретиков (**), многие подобные статьи основываются на шишках, набитых в реальном мире. Сегодня вы могли столкнуться с проблемами кривой реализации синглтона, завтра – с непонятным поведением изменяемых значимых типов, послезавтра вы прибиваете поток с помощью Thread.Abort и получаете рассогласованное состояние системы (***). Все эти проблемы весьма реальны и понимание принципов, заложенных в их основу может сэкономить денек другой при поиске какого-нибудь особенно злого бага.
Дополнительные ссылки
(**) Один мой коллега свято верит в то, что книги и статьи читать не имеет смысла, поскольку их пишут те, кто ничего не смыслит в настоящей разработке ПО. Поэтому он считает, что существует только одна «настоящая» реализация синглтона, и что нужно добавлять пустой финализатор всем классам, реализующим интерфейс IDisposable.