Можно ли обойтись без статического конструктора?

Можно ли обойтись без статического конструктора?

Мне понятно, что статический конструктор служит для присвоения значений статическим переменным, что он вызывается в первую очередь при создании объекта класса.

Но зачем он нужен, если я могу присвоить эти значения при объявлении переменных?

В языке C# существует несколько способов инициализации полей:

В месте объявления

Вот наивный пример:

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

Тут нужно сказать, почему у нас два конструктора. Конструктор экземпляра - это некоторая вспомогательная функция, призванная инициализировать экземпляр создаваемого объекта. Например, мы можем сказать, что любой валидный объект должен обладать некоторым поведением (инвариантом) и конструктор - это специальная функция, обязанная его обеспечить. Инвариантом может быть что угодно: начиная от того, что некоторое поле не нулевое, заканчивая более сложными правилами, например, что сумма полей дебет и кредит равна 0.

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

В отличие от конструктора объекта конструктор типа не вызывается пользователем. Вместо этого, он вызывается CLR в момент первого обращения к типу (опустим точные правила).

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

Разница в случае экземплярных полей и конструкторов

Все инициализаторы экземплярных полей перемещаются компилятором C# в экземплярных конструктор. Но главный вопрос здесь: куда именно.

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

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

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

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

Разница в случае статических полей и конструкторов

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

Но сложность возникает с тем, когда именно будет вызван статический конструктор.

Как уже написал @Qwertiy наличие или отсутствие статического конструктора в классе влияет на то, когда будет вызван этот самый конструктор. Наличие статического конструктора приводит к генерации странного специального флага, который затем скажет CLR, что можно более вольно относиться к времени вызова статического конструктора, и сделать это теперь можно будет не прямо перед первым обращением, а, например, перед вызовом метода, в котором это обращение происходит.

Потенциально, это может повлиять на эффективность приложения, поскольку теперь проверка будет делаться один раз, а не тысячи раз, при условии, что первое обращение находится в цикле от 0 до 1000.

Заключение

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

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

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

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

📎📎📎📎📎📎📎📎📎📎