Когда происходит вызов статического конструктора в статическом классе

Обновлено: 28.04.2024

Давайте рассмотрим одну из наиболее простых и популярных реализаций паттерна Синглтон (*), основанную на инициализаторе статического поля:

public sealed class Singleton
private static readonly Singleton instance = new Singleton();

public static Singleton Instance < get < return instance; >>
>

* This source code was highlighted with Source Code Highlighter .

Потокобезопасность этой реализации основывается на том, что статический конструктор (или, другими словами, инициализатор типа) в одном домене гарантировано вызывается не более одного раза. А раз так, то со стороны разработчика не нужно делать никакие финты ушами ради того, чтобы сделать потокобезопасность еще более безопасной. Большинство разработчиков (и до недавнего времени и я в том числе) на этом успокаиваются, поскольку основную проблему, которая может произойти с любым синглтоном, мы уже решили, так что на комментарий пустого статического конструктора мало кто обращает внимание. А поскольку этот комментарий вообще нифига не понятен, то пустой статический конструктор просто не доходит до многих реализаций в реальных приложениях.

Статический конструктор и инициализаторы полей

Итак, давайте рассмотрим следующий код:

public static string S = Echo( "Field initializer" );

public static string Echo( string s)
Console .WriteLine(s);
return s;
>
>

static void Main( string [] args)
Console .WriteLine( "Starting Main. " );
if (args.Length == 1)
Console .WriteLine(Singleton.S);
>
Console .ReadLine();
>
>

* This source code was highlighted with Source Code Highlighter .

Как видно, что статическое поле будет проинициализировано, хотя сам тип в приложении не используется. Практика показывает, что в большинстве случаев при отсутствии явного конструктора, JIT-компилятор вызывает инициализатор статических переменных непосредственно перед вызовом метода, в котором используется эта переменная. Если раскомментировать статический конструктор класса Singleton, то поведение будет именно таким, которое ожидает большинство разработчиков – инициализатор поля вызван не будет и при запуске приложения на экране будет только одна строка: Starting Main…”.

Статические конструкторы и взаимоблокировка

Поскольку статический конструктор указанного типа должен вызываться не более одного раза в домене приложения, то CLR вызывает его внутри некоторой блокировки. Тогда, если поток, исполняющий статический конструктор будет ожидать завершения другого потока, который в свою очередь попытается захватить ту же самую внутреннюю блокировку CLR, мы получим классический дедлок.

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

class Program
static Program()
var thread = new Thread(o => < >);
thread.Start();
thread.Join();
>

static void Main()
// Этот метод никогда не начнет выполняться,
// поскольку дедлок произойдет в статическом
// конструкторе класса Program
>
>

* This source code was highlighted with Source Code Highlighter .

Причем, если обратиться к спецификации 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()
<>

>

* This source code was highlighted with Source Code Highlighter .

Заключение

Дополнительные ссылки

(**) Один мой коллега свято верит в то, что книги и статьи читать не имеет смысла, поскольку их пишут те, кто ничего не смыслит в настоящей разработке ПО. Поэтому он считает, что существует только одна «настоящая» реализация синглтона, и что нужно добавлять пустой финализатор всем классам, реализующим интерфейс IDisposable.

(***) Если интересно, какие такие проблемы таятся в изменяемых значимых типах, то вполне подойдет предыдущая заметка «О вреде изменяемых значимых типов», ну а если интересно, что же такого плохого в вызове Thread.Abort, то тут есть даже две заметки: «О вреде вызова Thread.Abort», а также перевод интересной статьи Криса Селлза «Изучение ThreadAbortExcpetion с помощью Rotor».

Статический класс в основном такой же, как и нестатический класс, но имеется одно отличие: нельзя создавать экземпляры статического класса. Другими словами, нельзя использовать оператор new для создания переменной типа класса. Поскольку нет переменной экземпляра, доступ к членам статического класса осуществляется с использованием самого имени класса. Например, если есть статический класс, называемый UtilityClass , имеющий открытый статический метод с именем MethodA , вызов метода выполняется, как показано в следующем примере:

Ниже приведены основные возможности статического класса.

Содержит только статические члены.

Создавать его экземпляры нельзя.

По сути, создание статического класса аналогично созданию класса, содержащего только статические члены и закрытый конструктор. Закрытый конструктор не допускает создания экземпляров класса. Преимущество применения статических классов заключается в том, что компилятор может проверить отсутствие случайно добавленных членов экземпляров. Таким образом, компилятор гарантирует невозможность создания экземпляров таких классов.

Статические классы запечатаны, поэтому их нельзя наследовать. Они не могут наследовать ни от каких классов, кроме Object. Статические классы не могут содержать конструктор экземпляров. Однако они могут содержать статический конструктор. Нестатические классы также должен определять статический конструктор, если класс содержит статические члены, для которых нужна нетривиальная инициализация. Дополнительные сведения см. в разделе Статические конструкторы.

Пример

Ниже приведен пример статического класса, содержащего два метода, преобразующих температуру по Цельсию в температуру по Фаренгейту и наоборот.

Статический члены

Нестатический класс может содержать статические методы, поля, свойства или события. Статический член вызывается для класса даже в том случае, если не создан экземпляр класса. Доступ к статическому члены всегда выполняется по имени класса, а не экземпляра. Существует только одна копия статического члена, независимо от того, сколько создано экземпляров класса. Статические методы и свойства не могут обращаться к нестатическим полям и событиям в их содержащем типе, и они не могут обращаться к переменной экземпляра объекта, если он не передается явно в параметре метода.

Более привычно объявление нестатического класса с несколькими статическими членами, чем объявление всего класса как статического. Статические поля обычно используются для следующих двух целей: хранение счетчика числа созданных объектов или хранение значения, которое должно совместно использоваться всеми экземплярами.

Статические методы могут быть перегружены, но не переопределены, поскольку они относятся к классу, а не к экземпляру класса.

Несмотря на то, что поле не может быть объявлено как static const , поле const по своему поведению является статическим. Он относится к типу, а не к экземплярам типа. Поэтому к полям const можно обращаться с использованием той же нотации ClassName.MemberName , что и для статических полей. Экземпляр объекта не требуется.

Для объявления статических методов класса используется ключевое слово static перед возвращаемым типом члена, как показано в следующем примере:

Статические члены инициализируются перед первым доступом к статическому члену и перед вызовом статического конструктора, если таковой имеется. Для доступа к члену статического класса следует использовать имя класса, а не имя переменной, указывая расположение члена, как показано в следующем примере:

Если класс содержит статические поля, должен быть указан статический конструктор, который инициализирует эти поля при загрузке класса.

Вызов статического метода генерирует инструкцию вызова в промежуточном языке Microsoft (MSIL), в то время как вызов метода экземпляра генерирует инструкцию callvirt , которая также проверяет наличие ссылок на пустые объекты. Однако в большинстве случаев разница в производительности двух видов вызовов несущественна.

Общая концепция
  • полям
  • свойствам
  • методам
  • операторам
  • событиям
  • конструктору
  • классам

Статика подразумевает, что вам не нужно создавать экземпляр класса. Все приведённые выше составляющие класса, доступны посредством указания его имени.

Следует отметить, что необязательно делать весь класс статическим. Иногда достаточно применить статику для отдельных его членов.

  • Нельзя создавать экземпляр класса, используя ключевое слово new.
  • Не разрешается использовать не статические члены этого же класса.
  • Он не поддерживает наследование.
  • Невозможно перегрузить методы.
  • Не разрешается использовать не статические члены этого же класса из статических. Конечно же, вам никто не мешает создать экземпляр класса в статическом методе.
  • Наследование и полиморфизм для статических членов не поддерживаются.
Больше деталей

Выше мы не рассматривали такую конструкцию, как статический конструктор. Один из достаточно интересных вопросов, на мой взгляд, когда происходит вызов статического конструктор у классов?

Я думаю вы уже обратили внимание, что для статического конструктора не используется спецификатор доступа. Всё очень очевидно, создание статики вы не контролируете. Если попробовать выполнить приведённый ниже код, то можно убедиться в верности следующего утверждения: статический конструктор вызывается перед доступом к любому члену класса.


Можно поиграться, закомментировав любую из строк в которых происходит обращение к классу Box. Теперь немного изменим код, и подправим наше утверждение

Общие рассуждения об использовании статики

Существует относительно много мнений, когда использовать статические классы и когда не стоит так поступать. Исходя из своего опыта, отмечу, статические классы — любимое оружие начинающих разработчиков. Попользовал и забыл — хорошая концепция.
Чтобы разобраться в хитросплетениях о применяемости статики, следует вернуться к понятиям ООП. Представьте что у вас есть велосипед, но велосипед есть так же и у вашего соседа, и у соседа соседа, и т.д. В данном случае статика неприемлема. Т.к. велосипеды могут быть разного цвета, веса, обладать разным количеством колёс. То-бишь различные экземпляры одного и того же вида. Статика же применима для каких-то глобальных объектов\действий, когда не подразумевается создание экземпляров класса(часто для каких-то служебных методов: вывод на консоль — Console.WriteLine(), сортировка массива Array.Sort). Зачастую классы могут предоставлять как статическую, так и не статическую функциональность. Когда же у вас возникают сомнения, остановитесь и подумайте, понадобится ли вам экземпляр “этого”. Если же вы так хотите контролировать создание экземпляров класса или же вообще иметь только один, то для этих целей замечательно пригодится паттерн Singleton. В рамках ООП статика обладает рядом недостатков. Чем же она так плоха?

Полиморфизм

Как упоминалось выше, статические классы не поддерживают наследование, т.е. вы не можете наследоваться от интерфейса или другого класса и таким образом расширить функциональность.

Тестирование

При использование статики тестирование достаточно затруднено. Нельзя оперативно подменять код, основываясь на интерфейсах. Если нужно менять, то серьёзно, переписывая значительные куски кода.

Единственная ответственность


Статические методы, зачастую, применяются для служебных целей, будь-то вывод в лог ошибок или сортировка массива. Поэтому иногда возникает желание запихнуть всю статическую функциональность в один класс, обозвав его MegaUtils. Так поступать не стоит, лучше создать целую кучу маленьких классов, отвечающих каждый за свою область деятельности.

Читая книгу Эккеля, натолкнулся на один момент, который не могу понять. Представлен следующий код:

Всегда думал, что перед вызовом конструктора, должны проиницилизоваться поля класса, чтобы в случае обращения к неициализирвоанной переменной в конструкторе не получить исключение. Следуя этой логике вывод на экран надписи "Beetle.k initialized" должен был быть после вывода "Beetle constructor" . Прощу помочь разобраться. Спасибо.

3 ответа 3

Порядок инициализации таков:

  1. Статические элементы родителя
  2. Статические элементы наследника
  3. Глобальные переменные родителя
  4. Конструктор родителя
  5. Глобальные переменные наследника
  6. Конструктор наследника

Пример

Еще было бы неплохо указать, что статические переменные(да и вообще весь статический контент) инициализируется когда классЛоадер впервые натыкается на этот класс. А все другие переменные уже при вызову конструктора.

@Nara всё верно. Если это не делается вручную, то компилятор сам прописывает вызов конструктора родителя без аргументов.

@Nara, вас смутило то, что я в порядке указал конструктор родителя раньше наследника? Дело в том, что первым делом, при чтении конструктора наследника, вызывается конструктор родителя. Так что справедливее писать именно так.

Порядок инициализации экземпляра объекта описан в JLS (12.5 Creation of New Class Instances) (перевод мой):

  1. Присвоить аргументы конструктора переменным-параметрам для вызова этого конструктора.
  2. Если конструктор начинается с явного вызова (§8.8.7.1) другого конструктор этого же класса (с использованием this ), то нужно вычислить аргументы и выполнить тот конструктор рекурсивно, используя эти же 5 шагов. Если выполнение того конструктора будет прервано (completes abruptly), то этот алгоритм будет прерван по тем же причинам, иначе перейти к шагу 5.
  3. Если конструктор не начинается с явного вызова другого конструктора этого же класса (с использованием this ), то для классов, отличных от Object явно или неявно вызывается конструктор суперкласса (используя super ). Нужно вычислить аргументы и выполнить конструктор суперкласса рекурсивно, используя эти же 5 шагов. Если выполнение того конструктора будет прервано (completes abruptly), то этот алгоритм будет прерван по тем же причинам, иначе перейти к шагу 4.
  4. Выполнить инициализаторы экземпляра (instance initializers) и инициализаторы переменных экземпляра (instance variable initializers) для этого класса, с присвоением значений инициализаторов переменных экземпляра соответствующим переменным экземпляра, слева на право в порядке появления в исходном коде класса. Если выполнение любого инициализатора вызывает исключение, следующие инициализаторы не обрабатываются, а этот алгоритм завершается с тем же исключением. Иначе перейти к шагу 5.
  5. Выполнить остальное тело конструктора. Если выполнение будет прервано, то этот алгоритм будет прерван по тем же причинам. Иначе алгоритм завершится нормально.
  • T - класс и создается экземпляр класса T .
  • Вызов статического метода, объявленного в T .
  • Выполняется присваивание статическому полю, объявленному в T .
  • Статическое поле, объявленное в T используется, и это поле не является константой (§4.12.4) (константа - final переменная примитивного типа или String , объявленная с инициализатором, являющимся константным выражением)
  • T - класс верхнего уровня (§7.6) и выполняется выражение assert (assert statement) (§14.10) лексически расположенное внутри T (§8.1.3).

Вооружившись этим корявым переводом, посмотрим, что происходит в вашем примере:


Кроме обычных методов и полей класс может иметь статические поля, методы, константы и инициализаторы. Например, главный класс программы имеет метод main, который является статическим:

Для объявления статических переменных, констант, методов и инициализаторов перед их объявлением указывается ключевое слово static .

Статические поля

При создании объектов класса для каждого объекта создается своя копия нестатических обычных полей. А статические поля являются общими для всего класса. Поэтому они могут использоваться без создания объектов класса.

Например, создадим статическую переменную:

Класс Person содержит статическую переменную counter, которая увеличивается в конструкторе и ее значение присваивается переменной id. То есть при создании каждого нового объекта Person эта переменная будет увеличиваться, поэтому у каждого нового объекта Person значение поля id будет на 1 больше чем у предыдущего.

Так как переменная counter статическая, то мы можем обратиться к ней в программе по имени класса:

Консольный вывод программы:

Статические константы

Также статическими бывают константы, которые являются общими для всего класса.

Стоит отметить, что на протяжении всех предыдущих тем уже активно использовались статические константы. В частности, в выражении:

out как раз представляет статическую константу класса System. Поэтому обращение к ней идет без создания объекта класса System.

Статические инициализаторы

Статические инициализаторы предназначены для инициализации статических переменных, либо для выполнения таких действий, которые выполняются при создании самого первого объекта. Например, определим статический инициализатор:

В самой программе создаются два объекта класса Person. Поэтому консольный вывод будет выглядеть следующим образом:

Стоит учитывать, что вызов статического инициализатора производится после загрузки класса и фактически до создания самого первого объекта класса.

Статические методы

Статические методы также относятся ко всему классу в целом. Например, в примере выше статическая переменная counter была доступна извне, и мы могли изменить ее значение вне класса Person. Сделаем ее недоступной для изменения извне, но доступной для чтения. Для этого используем статический метод:

Теперь статическая переменная недоступна извне, она приватная. А ее значение выводится с помощью статического метода displayCounter. Для обращения к статическому методу используется имя класса: Person.displayCounter() .

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

Вообще методы определяются как статические, когда методы не затрагиют состояние объекта, то есть его нестатические поля и константы, и для вызова метода нет смысла создавать экземпляр класса. Например:

В данном случае для методов sum, subtract, multiply не имеет значения, какой именно экземпляр класса Operation используется. Эти методы работают только с параметрами, не затрагивая состояние класса. Поэтому их можно определить как статические.

Читайте также: