Автор: Sergey Teplyakov
Статические конструкторы являются одной из самых странных возможностей C# и CLR и многие годы я не понимал их достаточно хорошо, но поскольку Джон (Скит) внес дополнительные разъяснения в мое понимание этой возможности, я все еще явно не до конца ее понимаю!Эрик Липперт Static constructors, part three
Вопрос: в каком порядке будут вызваны экземплярные и статические конструкторы классов B и D при создании экземпляра класса D?class B { }class D : B { }
Ответ: конструкторы экземпляров вызываются от базового к наследнику, а статические конструкторы it depends!
О порядке вызова статических конструкторов
Согласно спецификации ECMA CLI Specification (именно там описаны вещи, связанные с поведением CLR) статический конструктор или инициализатор типа должен быть вызван не более одного раза, что приводит к замечательным последствиям в виде потенциальных взаимоблокировок, описанных в прошлый раз.
При этом существует две гарантии вызова статических конструкторов:
- Базовая гарантия инициализации типа: конструктор типа будет вызван перед созданием экземпляра или перед первым обращением к статическому члену.
- 'Расслабленная' (relaxed) гарантия инициализация типа (BeforeFieldInit): конструктор типа будет вызван при первом обращении к статическому полю (справедливо для .NET 4.0+) и может быть вызван когда угодно до первого обращения к типу.
При этом, когда дело касается наследования, среда исполнения не требует, чтобы статический конструктор базового класса вызывался до вызова статического конструктора наследников (на самом деле, вызов статического метода наследника вообще не приводит к вызову статического конструктора базового класса).
Для того, чтобы CLR использовала расслабленную модель, тип должен быть помечен флагом BeforeFieldInit. Для языка C# выбор определяется наличием или отсутствием явного статического конструктора: при наличии явного статического конструктора используется базовая модель инициализации типа, а при отсутствии статического конструктора расслабленная модель.
Базовая модель инициализации типа
Итак, что мы получим при запуске следующего кода:class B{static B() { Console.WriteLine('B.cctor'), }public B() { Console.WriteLine('B.ctor'), }}class D : B{static D() { Console.WriteLine('D.cctor'), }public D() { Console.WriteLine('D.ctor'), }}internal class Program{public static void Main() {Console.WriteLine('Main'),new D(), }}
Поскольку мы явно объявили статический конструктор, то порядок вызова попадает под базовую модель инициализации типа, а это значит, мы увидим на экране следующий вывод:
MainD.cctorB.cctorB.ctorD.ctor
Напомню, что порядок вызова конструктора экземпляра определяется компилятором языка C# и на псевдокоде конструктор класса D может быть представлен следующим образом:public D(){// Вызов инициализаторов полей ExecuteFieldInitializers(),// Вызываем конструктор базового класса B.ctor(),// Отсальное тело конструктора класса D D.ctor(),}
Поскольку вначале все же физически вызывается конструктор класса D, а потом уже конструктор класса B, то и статический конструктор класса D должен вызываться до статического конструктора класса B.
Расслабленная модель инициализации типа
Итак, при наличии статических конструкторов единственным 'спорным' момментом является порядок вызова статических конструкторов базового класса и наследников. Но при отсутствии явного статического конструктора ситуация усложняется.
ПРИМЕЧАНИЕВсе трюки, расписанные ниже нужно проверять нужно в Release режиме и без подклченного отладчика (запуская консольное приложение командой 'Debug ->, Start Without Debugging').
Время вызова статического конструктора
Первым, и достаточно известным изменением является время вызова статического конструктора. Так, при запуске следующего кода мы получим 'S.cctor', а затем 'Main':class Helper{public static string GetString(string s) {Console.WriteLine(s),return s, }}class S{private static string _foo = Helper.GetString('S.cctor'),private static Lazy<,S>, _instance = new Lazy<,S>,(() =>, new S()),public static S Instance { get { return _instance.Value, } }}internal class Program{public static void Main() {Console.WriteLine('Main'),var s = S.Instance, }}
Поскольку первое обращение к типу может производиться в цикле, то иногда эффективнее проиницилизировать тип еще перед вызовом метода, в котором происходит первое обращение и расслабленная модель позволяет использовать эту оптимизацию.
'Расслабленная' модель (.NET 4.0 +)
А теперь давайте посмотрим, что будет при вызове следующего кода:class B{static string _field = Helper.GetString('B.cctor'),public B() { Console.WriteLine('B.ctor'), }}class D : B{static string _field = Helper.GetString('D.cctor'),public D() { Console.WriteLine('D.ctor'), }public static void Pure() { Console.WriteLine('Pure'), }public static void UsesStaticField() {Console.WriteLine('Before accessing _field'),Console.WriteLine('Field: {0}', _field), }}internal class Program{public static void Main() {Console.WriteLine('Main'),var d = new D(),D.Pure(),D.UsesStaticField(), }}
При запуске в Release режиме и без подключенного отладчика мы получим следующий результат:
MainB.ctorD.ctorPureD.cctorBefore accessing _fieldField: D.cctor
Да, в результате мы создаем экземпляр класса D, а статический конструктор класса D не вызывается! Мы можем создать экземпляр класса, вызвать экземплярный или статический метод, и все это не приведет к вызову статического конструктора, если эти методы не обращаются к статическим полям!
'Расслабленная' гарантия ведет себя максимально 'отложено', в результате статический конструктор вызывается лишь в случае крайней необходимости: при доступе к статическим полям объекта.
Зачем мне это нужно?
Время вызова статического конструктора определяется довольно сложными правилами, к тому же существует 'расслабленная' модель вызова, поведение которой зависит от реализации. В большинстве случаев статические конструкторы содержат небольшое количество кода, который инициализирует статическое состояние. В этом случае порядок и время вызова не играет никакой роли.
Если же код, вызываемый в статическом конструкторе завязан на некоторые глобальные побочные эффекты, то непонимание правил вызова может привести к неочевидному поведению и сложно отлаживаемым ошибкам.
Дополнительные ссылки
- Взаимоблокировки в статических конструкторах
- О синглтонах и статических конструкторах