Почему локальные переменные в недостижимом коде полностью определены?

Вы, скорее всего, знакомы с возможностью языка C#, которая запрещает чтение локальной переменной до того, пока она не будет «полностью определена» (definitely assigned):

void M()
{
int x;
if (Q())
x = 123;
if (R())
Console.WriteLine(x); // Некорректно!
}

Этот код некорректен, поскольку существует путь исполнения, при выполнении которого локальная переменная будет прочитана до того, как ей будет присвоено значение; в данном случае, если метод Q() вернет false, а метод R() вернет true.

Причина некорректности данного кода заключается не в том, как многие думают, что локальная переменная инициализируется мусором, от которого мы хотим вас защитить. На самом деле, локальные переменные автоматически инициализируются значениями по умолчанию. (Хотя в языках С и С++ это не так, что позволяет прочитать мусор из не проинициализированных локальных переменных.) Связано это с тем, что существование подобного пути исполнения, скорее всего, является ошибкой в коде, а мы стараемся повернуть вас на путь качества; поэтому, чтобы допустить ошибку, вам придется постараться.

Способ определения компилятором существования пути исполнения, при котором переменная x будет прочитана перед ее записью, интересен сам по себе, но это тема для другой статьи. Сегодня же я хочу рассмотреть следующий вопрос: почему локальные переменные считаются полностью определенными (definitely assigned) внутри недостижимого кода?

void M()
{
int x;
if (Q())
x = 123;
if (false)
Console.WriteLine(x); // Все ОК!
}

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

С другой стороны: этот код выглядит неправильно. Мы не допускаем в недостижимом коде синтаксических ошибок или ошибок определения перегруженных версий методов, или чего-то еще, так почему мы допускаем ошибки полного определения?

Честно признаюсь, что это очень сложная ситуация. Вот в чем дело. Прежде всего, вы должны задать себе вопрос: «а почему в моем коде есть недостижимый код?» Это так и задумано или это ошибка.

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

А может ли вообще кому-то понадобиться явно оставлять недостижимый код? На самом деле, такое бывает; причем довольно часто, когда имеешь дело с незаконченной библиотекой другой команды:

void BlorgFrob(Frob frob)
{
IBlorgable blorgable;
// TODO: Glob.TryResrov еще не портирован на C#.
if (false /* Glob.TryResrov(out blorgable, frob) */)
{
BlorgGlob(blorgable);
}
else
{
blorgable = Blob.Resrov(frob)
BlorgBlob(blorgable);
}
blorgable.Fribble(frob);
}

Должен ли вызов BlorgGlob(blorgable) компилироваться? Кажется вполне логичным, что ошибки здесь нет; в конце концов, ведь локальная переменная никогда не будет прочитана. Но все равно хорошо, что в невызываемом коде мы будем получать ошибки определения перегруженных версий метода, просто на тот случай, если там будет написано что-то не то.

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