Когда инициализируется static класс в C#
Что проверяют
Знаете ли вы, что момент инициализации зависит от того, объявлен ли у класса статический конструктор, и понимаете ли, что это даёт в многопоточном коде. Следом обычно спрашивают, что будет, если статический конструктор бросит исключение.
Как отвечать
Сначала разведите два события: загрузку сборки и инициализацию типа. Сборка подгружается, когда JIT компилирует метод, который ссылается на тип. Инициализация типа (статические поля и статический конструктор) происходит по своим правилам:
- есть явный
staticконструктор: он выполняется ровно один раз, непосредственно перед первым обращением к любому статическому члену. Момент детерминирован; - конструктора нет: компилятор помечает тип флагом
beforefieldinit, и среда вправе инициализировать поля в любой момент до первого чтения статического поля. Вызов метода, который поля не трогает, инициализацию может не запустить вовсе; - порядок: сначала инициализаторы полей в порядке объявления, затем тело статического конструктора;
- потокобезопасность: CLR гарантирует однократный запуск под внутренней блокировкой, остальные потоки ждут. Поэтому
static readonlyполе уже безопасный синглтон, а для явной ленивости естьLazy<T>; - исключение: превращается в
TypeInitializationException, и тип остаётся непригодным до конца жизни процесса, повторной попытки не будет.
Полезно добавить, что у обобщённого статического класса каждый закрытый тип (Cache<int>, Cache<string>) инициализируется отдельно, а const подставляется при компиляции и инициализацию не вызывает.
Где спотыкаются
Говорят «статический класс инициализируется при старте программы». Нет: без обращения к нему инициализация не случится вовсе.
Не знают про beforefieldinit и полагаются на побочные эффекты в инициализаторах полей. Логирование или регистрация срабатывают не тогда, когда ожидали, а добавление пустого статического конструктора внезапно меняет поведение.
Кладут в статический конструктор тяжёлую работу с сетью или конфигом. Одна ошибка при старте, и тип мёртв до перезапуска, а каждый вызов бросает TypeInitializationException с настоящей причиной во InnerException.