среда, 10 июля 2013 г.

О времени вызова статических конструкторов

Статические конструкторы являются одной из самых странных возможностей 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();
    }
}

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

Main
D.cctor
B.cctor
B.ctor
D.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 режиме и без подключенного отладчика мы получим следующий результат:

Main
B.ctor
D.ctor
Pure
D.cctor
Before accessing _field
Field: D.cctor

Да, в результате мы создаем экземпляр класса D, а статический конструктор класса D не вызывается! Мы можем создать экземпляр класса, вызвать экземплярный или статический метод, и все это не приведет к вызову статического конструктора, если эти методы не обращаются к статическим полям!

"Расслабленная" гарантия ведет себя максимально "отложено", в результате статический конструктор вызывается лишь в случае крайней необходимости: при доступе к статическим полям объекта.

Зачем мне это нужно?

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

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

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

7 комментариев:

  1. Плохой тон, я считаю, завязываться на условностях компилятора в отношении вызова конструктора. Компилятор правильно себя ведёт, конструктор нужен, только тогда, когда идёт обращение к полям. Если приходится учитывать эти особенности при разработке, значит разработка кривая. Пока примеров, где-бы это реально потребовалось не знаю. Драйвера я конечно не разрабатывал и сверх-тяжёлой оптимизацией не занимался, но честно, трудно представить, зачем это нужно знать, только если чтобы тесты какие-нибудь пройти :)

    ОтветитьУдалить
  2. @Алексей: вы правы в том, что эти знания *редко* нужны на практике, однако это не значит, что они не пригодятся. Я, например, провел несколько дней отлаживая проблему, связанную с ранним вызовом статического конструктора, которая описана здесь.

    Ну и конечно же, в основном подобные статьи для меня - это такой себе brain teaser.

    ОтветитьУдалить
  3. А все равно большое спасибо, знания не рюкзак, за собой не носить) Хорошо пишете) НА досуге почитайте мои статейки, если время будет)))

    ОтветитьУдалить
  4. Да, в результате мы создаем экземпляр класса D, а статический конструктор класса D не вызывается!
    Не совсем очевидно, что понимается под статическим конструктором. Если это "расслабленная" модель - то статический конструктор отсутствует по определению, и класс помечен атрибутом BeforeFieldInit. И тип инициализируется перед первым обращением к статическому полю (хотя в реальности это зависит от реализации). Т.е. подразумевается ли, что инициализация типа и вызов статического конструктора одно и то же событие?

    ОтветитьУдалить
    Ответы
    1. В данном случае под статическим конструктором я имел ввиду побочный эффект инициализации статических полей.
      Т.е. если запустить этот код в релизе и без подключенного отладчика, то будет видно, что конструктор экземпляра отработал, а D.cctor выведено на экран не будет.

      Удалить
    2. Это да. Понятно. Но все же не стала бы называть "внутренний" инициализатор типа статическим конструктором. И спасибо за качественный материал, Сергей :)

      Удалить
    3. Марина, я не совсем понял, что такое "внутренний" инициализатор типа.
      Я имел ввиду, что статические поля по своей сути инициализируются в статическом конструкторе.

      Удалить