Почему CLR не всегда вызывает конструкторы типа значения
У меня есть вопрос о конструкторах типа в типе значения. Этот вопрос был вдохновлен тем, что Джеффри Рихтер написал в CLR через С# 3rd ed, он говорит (на странице 195 - глава 8), что вы никогда не должны определять конструктор типа в пределах типа значения, поскольку есть моменты, когда CLR не будет звонить он.
Итак, например (на самом деле... пример Джеффри Рихтера), я не могу сработать, даже посмотрев на IL, почему конструктор типа не вызывается в следующем коде:
internal struct SomeValType
{
static SomeValType()
{
Console.WriteLine("This never gets displayed");
}
public Int32 _x;
}
public sealed class Program
{
static void Main(string[] args)
{
SomeValType[] a = new SomeValType[10];
a[0]._x = 123;
Console.WriteLine(a[0]._x); //Displays 123
}
}
Итак, применяя следующие правила для конструкторов типов, я просто не понимаю, почему конструктор типа значения выше не вызывается вообще.
- Я могу определить конструктор типа статического значения, чтобы установить начальное состояние типа.
- Тип может иметь не более одного конструктора - по умолчанию не существует.
- Конструкторы типов неявно закрыты
- Компилятор JIT проверяет, был ли уже создан конструктор типа в этом AppDomain. Если он не отправляет вызов в собственный код, иначе он не знает, что тип уже инициализирован.
Итак... Я просто не могу понять, почему я не вижу, как этот массив типов создается.
Мое лучшее предположение, что это может быть:
- Способ, которым CLR создает массив типов. Я бы подумал, что статический конструктор будет вызываться при создании первого элемента.
- Код в конструкторе не инициализирует статические поля, поэтому он игнорируется. Я экспериментировал с инициализацией частных статических полей внутри конструктора, но поле остается значением по умолчанию 0 - поэтому конструктор не вызывается.
- Или... компилятор каким-то образом оптимизирует вызов конструктора из-за установленного публичного Int32, но в лучшем случае это нечеткое предположение.
Лучшие практики и т.д., я просто очень заинтригован этим, потому что хочу, чтобы я мог сам убедиться, почему он не вызван.
EDIT: Я добавил ответ на свой вопрос ниже, просто цитату из того, что Джеффри Рихтер говорит об этом.
Если у кого-то есть какие-то идеи, тогда это будет блестяще.
Большое спасибо,
Джеймс
Ответы
Ответ 1
Microsoft С# 4 Spec немного изменился с предыдущих версий и теперь более точно отражает поведение, которое мы здесь видим:
11.3.10 Статические конструкторы
Статические конструкторы для структур большинство тех же правил, что и для классов. Выполнение статического конструктора для типа структуры инициируется первое из следующих событий в домене приложения:
- Указывается статический член типа struct.
- Вызывается явно объявленный конструктор типа struct.
Создание значений по умолчанию (§11.3.4) типов структур не запускает статический конструктор. (An примером этого является начальное значение элементов в массиве.)
ECMA Spec и Microsoft С# 3 Spec оба имеют дополнительное событие в этом списке: "Указывается член экземпляра типа struct". Итак, похоже, что С# 3 противоречит его собственной спецификации. С# 4 Spec был приведен в более близкое соответствие с фактическим поведением С# 3 и 4.
ИЗМЕНИТЬ...
После дальнейшего исследования выяснилось, что почти весь доступ к членам экземпляра, кроме прямого доступа к полю, вызовет статический конструктор (по крайней мере, в текущих реализациях Microsoft С# 3 и 4).
Таким образом, текущие реализации более тесно коррелируют с правилами, указанными в спецификациях ECMA и С# 3, чем те, что указаны в спецификации С# 4: правила С# 3 правильно реализованы при доступе ко всем членам экземпляра, кроме полей; правила С# 4 реализованы правильно для доступа к полям.
(Различные спецификации согласуются - и, по-видимому, правильно реализованы - когда речь идет о правилах, касающихся доступа к статическому члену и явно объявленных конструкторов.)
Ответ 2
Из § 18.3.10 стандарта (см. также Язык программирования С#):
Выполнение статического конструктора для структуры инициируется первым из следующих событий в домене приложения:
- Член экземпляра структуры ссылки.
- Статический член ссылка на структуру.
- Явным образом объявленный конструктор struct.
[Примечание: создание значений по умолчанию (§18.3.4) структуры типы не запускают статические конструктор. (Примером этого является начальное значение элементов в массив.) end note]
Итак, я согласен с вами в том, что последние две строки вашей программы должны запускать первое правило.
После тестирования консенсус, похоже, заключается в том, что он последовательно запускает методы, свойства, события и индексаторы. Это означает, что он правильный для всех явных членов экземпляра, кроме полей. Поэтому, если для стандарта были выбраны правила Microsoft С# 4, это приведет к тому, что их реализация будет в основном правильной, главным образом, неправильной.
Ответ 3
Обновление:. Мое наблюдение заключается в том, что если статическое состояние не используется, статический конструктор никогда не будет затронут - то, что среда исполнения, похоже, решит и не относится к ссылочным типам. Это вызывает вопрос о том, осталась ли ошибка, поскольку она мало влияет на нее, по дизайну или на ожидании ошибки.
Обновить 2: лично, если вы не делаете что-то в стиле конструктора, это поведение из среды выполнения никогда не должно вызывать проблемы. Как только вы получите доступ к статическому состоянию, он ведет себя правильно.
Update3: дальше к комментарию от LukeH и ссылаясь на ответ Мэтью Флашен, реализация и вызов вашего собственного конструктора в структуре также запускает вызывающий статический конструктор. Это означает, что в одном из трех сценариев поведение не то, что он говорит на жестяной ленте.
Я просто добавил статическое свойство к типу и получил доступ к этому статическому свойству - он назывался статическим конструктором. Без доступа к статическому свойству, просто создав новый экземпляр типа, статический конструктор не был вызван.
internal struct SomeValType
{
public static int foo = 0;
public int bar;
static SomeValType()
{
Console.WriteLine("This never gets displayed");
}
}
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// Doesn't hit static constructor
SomeValType v = new SomeValType();
v.bar = 1;
// Hits static constructor
SomeValType.foo = 3;
}
}
Примечание в этой ссылке указывает, что статические конструкторы называются not при простом доступе к экземплярам:
http://www.jaggersoft.com/pubs/StructsVsClasses.htm#default
Ответ 4
Это сумасшедшее по дизайну поведение атрибута "beforefieldinit" в MSIL. Это также влияет на С++/CLI, я подал отчет об ошибке, в котором Microsoft очень хорошо объяснила, почему поведение так оно и есть, и я указал на несколько разделов в стандарте языка, которые не соглашались/нуждались в обновлении, чтобы описать фактическое поведение, Но это не общедоступно. Во всяком случае, вот последнее слово на нем от Microsoft (обсуждение аналогичной ситуации в С++/CLI):
Поскольку мы ссылаемся на стандарт здесь строка из раздела I, 8.9.5 говорит следующее:
Если отмечено значение BeforeFieldInit, тогда выполняется метод инициализации типов. в, или когда-то раньше, первый доступ для любого статического поля, определенного для этого тип.
Этот раздел действительно подробно излагается о том, как реализация языка может выбрать, чтобы предотвратить поведение вы описываете. С++/CLI выбирает не к, скорее они позволяют программисту сделать это, если они пожелают.
В принципе, поскольку приведенный ниже код абсолютно никаких статических полей, JIT абсолютно правильно, просто не вызывая статические конструкторы классов.
То же поведение - это то, что вы видите, хотя на другом языке.
Ответ 5
Еще один интересный пример:
struct S
{
public int x;
static S()
{
Console.WriteLine("static S()");
}
public void f() { }
}
static void Main() { new S().f(); }
Ответ 6
Просто поместив это как "ответ", чтобы я мог поделиться тем, что написал сам мистер Рихтер (у кого есть ссылка на последнюю версию CLR, кстати, ее легко получить в 2006 году, но найти ее бит сложнее получить последний):
Для такого рода вещей обычно лучше смотреть на спецификацию CLR, чем на спецификацию С#. Спецификация CLR говорит:
4. Если не указано значение BeforeFieldInit, то этот тип инициализационного метода выполняется (т.е. Запускается):
• первый доступ к любому статическому полю этого типа или
• первый вызов любого статического метода этого типа или
• первый вызов любого экземпляра или виртуального метода этого типа, если он является типом значения или
• первый вызов любого конструктора для этого типа.
Поскольку ни одно из этих условий не выполняется, статический конструктор не вызывается. Единственные интересные моменты, которые следует отметить, это то, что "_x" - это поле экземпляра, а не статическое поле, а построение массива структур не вызывает никаких конструкторов экземпляров для элементов массива.
Ответ 7
Я бы предположил, что вы создаете ARRAY своего типа значений. Таким образом, новое ключевое слово будет использоваться для инициализации памяти для массива.
Имеет смысл сказать
SomeValType i;
i._x = 5;
без какого-либо нового ключевого слова где-нибудь, что по сути является тем, что вы здесь делаете. Если SomeValType является ссылочным типом, вам нужно будет инициализировать каждый элемент вашего массива с помощью
array[i] = new SomeRefType();