Аномалия инициализации

Дополнение: Я выяснил, что эта проблема значительно более загадочна, чем изначально думал исходя из отчета об ошибке. Я переписал примеры в этой статье, поскольку предыдущие примеры на самом деле не иллюстрировали ошибку.

Рассмотрим следующий код:

struct S
{
private string blah;
public S(string blah)
{
this.blah = blah;
}
public void Frob()
{ // что угодно
}
}

Следующий фрагмент кода является корректным (хотя и не слишком разумным):

S s1 = new S();
s1.Frob();

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

А как насчет такого кода?

S s2;
s2.Frob();

Похоже на ошибку обращения к неинициализированной переменной, так ведь?

Интересный и малоизвестный факт, относящийся к компилятору C#, заключается в том, что это сообщение об ошибке будет выдано, только если структура компилируется из исходного кода, но не тогда, когда структура находится в другой сборке! Какова причина такого, казалось бы аномального поведения?

Давайте рассмотрим следующую похожую ситуацию:

struct SS
{
  public int x;
  public int y;
  public void Bar() {…}
}

Здесь мы имеем чистейшее зло в виде изменяемого значимого типа. Корректен ли следующий код?

SS ss;
ss.x = 123;
ss.y = 456;
ss.Bar();

Да, код корректен. Локальная переменная s состоит из двух полей: x и y. Если поля x и y проинициализированы, тогда переменная s считается проинициализированной. При проверке инициализации структуры компилятор на самом деле проверяет, известно ли, что каждое поле структуры проинициализировано. Предполагается, что вызов конструктора инициализирует все поля, но без вызова конструктора мы оперируем с каждым полем по отдельности и затем получаем результат. (И если поля также являются значимыми типами, тогда, конечно же, выполняем эту задачу рекурсивно для каждого из них.)

Аномалия происходит из-за того, что когда компилятор встречает исходный код рассматриваемого типа, он отслеживает инициализацию всех полей, определяет, что поле “s1.blah” не инициализировано и выдает сообщение об ошибке при обращении к переменной s1. Но когда рассматриваемый тип находится в другой сборке, компилятор игнорирует недоступные поля ссылочного типа. Мы видим, что все поля из требующих инициализации доступных полей проинициализированы нулем, поэтому объявляем, что переменная s1 полностью проинициализирована!

Дополнение: Изначально я думал, что это поведение является странным и нежелательным, хотя и оправданным. Однако я только что узнал, что по загадочным для меня причинам, компилятор игнорирует только недоступные поля ссылочного типа. Недоступные поля значимого типа все еще рассматриваются при определении полной инициализации. Эта странная непоследовательность говорит о том, что это ошибка, а не особенность. Продолжаю свое исходное сообщение…

Это поведение, несомненно, является странным. Является ли оно желательным или нет, вопрос спорный. Конечно можно сказать, что у пользователя нет никакой возможности сделать что-либо с закрытыми полями импортируемого типа; типа, автором которого он явно не является, не знает всех его деталей и не может изменить. Но если тип находится в исходном коде, человек запустивший компиляцию имеет представление о его внутреннем устройстве. Т.е. мы должны предоставить пользователю доказательства того, что поля, к которым у него нет доступа и о которых он ничего не знает, являются проинициализированными. Поэтому мы инициализируем их значениями по умолчанию и забываем о них.

Есть еще один аргумент (более убедительный для меня), который заключается в том, что мы должны быть последовательными и всегда рассматривать либо все поля, либо только доступные, а не выбирать один из вариантов в зависимости от деталей процесса компиляции. Я выбираю первый вариант: заставить пользователя вызвать конструктор по умолчанию или использовать оператор “default(T)”, если он действительно хочет получить значение структуры со значениями полей по умолчанию. Это делает намерения пользователя явными.

К сожалению, хотя я отстаивал позицию, что это поведение является, скорее всего, нежелательным, мы столкнулись с серьезной проблемой. В BCL существует множество структур без открытых полей и огромный объем существующего кода, который использует их не вызывая конструктор по умолчанию. Мы не пойдем на такое изменение, которое приведет к ошибкам компиляции без видимых на то преимуществ. Тем более, эта аномалия является безвредной: заставим мы вас написать “s1 = new S()” или нет, на практике все поля в любом случае будут проинициализированы значениями по умолчанию.

Но, в то же время, я не думаю, что мы должны вносить изменения, которые во всех случаях сделают код корректным, даже когда тип находится в исходном коде. Ради согласованного поведения эта неприятная ситуация становится еще хуже. Это очень похоже на понятие «глупой согласованности».

Оригинал статьи