Почему локальные переменные требуют инициализации, но полей нет?

Если я создаю bool в моем классе, просто что-то вроде bool check, по умолчанию он равен false.

Когда я создаю один и тот же bool в моем методе, bool check (а не внутри класса), я получаю сообщение об ошибке "использование непризнанной проверки локальных переменных". Почему?

Ответ 1

Ответы Юваля и Дэвида в основном правильные; подведение итогов:

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

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

Во-первых, для любой переменной, локальной или иной, на практике невозможно точно определить, назначена или не назначена переменная. Рассмотрим:

bool x;
if (M()) x = true;
Console.WriteLine(x);

Вопрос "назначается x"? эквивалентно "действительно ли M() возвращает true?" Теперь предположим, что M() возвращает true, если Fermat Последняя теорема верна для всех целых чисел менее одиннадцати гаджонов, а false в противном случае. Чтобы определить, назначено ли x определенно, компилятор должен по существу представить доказательство последней теоремы Ферма. Компилятор не настолько умный.

Итак, что компилятор делает для локальных пользователей, реализует алгоритм, который выполняется быстро и переоценивает, когда локально определенно не назначается. То есть, он имеет некоторые ложные срабатывания, где говорится: "Я не могу доказать, что это местное назначено", даже если вы и я это знаем. Например:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Предположим, что N() возвращает целое число. Мы с вами знаем, что N() * 0 будет 0, но компилятор этого не знает. (Примечание: компилятор С# 2.0 знал об этом, но я удалил эту оптимизацию, поскольку в спецификации не сказано, что компилятор знает это.)

Хорошо, так что мы знаем до сих пор? Для локальных жителей нецелесообразно получать точный ответ, но мы можем дешево переоценить невыбранность и получить довольно хороший результат, который ошибается на стороне "заставить вас исправить вашу нечеткую программу". Это хорошо. Почему бы не сделать то же самое для полей? То есть, сделать определенную проверку правильности, которая переоценивает дешево?

Ну, сколько путей для инициализации локального? Его можно назначить в тексте метода. Он может быть назначен в лямбда в тексте метода; что лямбда никогда не может быть вызвана, поэтому эти присвоения не имеют отношения к делу. Или он может быть передан как "выход" в метод anothe, и в этот момент мы можем предположить, что он назначен, когда метод возвращается нормально. Это очень четкие точки, по которым назначается локальная сеть, и они находятся там же, что и локальный. Определение определенного задания для локальных жителей требует только локального анализа. Методы имеют тенденцию быть короткими - намного меньше, чем миллионы строк кода в методе - и поэтому анализ всего метода довольно быстрый.

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

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

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

Является ли ошибка компиляции этой библиотеки? Если да, то как BarCorp должен исправить ошибку? Назначив значение по умолчанию x? Но это то, что делает компилятор.

Предположим, что эта библиотека является законной. Если FooCorp пишет

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

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

Итак, почему бы не сделать более дешевый анализ, который не должен анализировать всю программу и просто переоценивать еще сильнее? Ну, предложите алгоритм, который работает, что не слишком сложно написать правильную программу, которая фактически компилируется, и команда разработчиков может ее рассмотреть. Я не знаю такого алгоритма.

Теперь комментатор предлагает "потребовать, чтобы конструктор инициализировал все поля". Это не плохая идея. На самом деле, такая неплохая идея, что С# уже имеет эту функцию для structs. Для конструктора структуры требуется определенно назначить все поля к тому времени, когда ctor вернется нормально; конструктор по умолчанию инициализирует все поля по умолчанию.

Как насчет классов? Ну, , как вы знаете, что конструктор инициализировал поле? Ctor может вызвать виртуальный метод для инициализации полей, и теперь мы вернулись в ту же позицию, в которой мы были раньше. Структуры не имеют производных классов; классы могут. Является ли библиотека, содержащая абстрактный класс, обязательной для того, чтобы содержать конструктор, который инициализирует все его поля? Как абстрактный класс знает, какие значения должны быть инициализированы полями?

Джон предлагает просто запретить вызовы методов в ctor перед инициализацией полей. Итак, подводя итоги, наши варианты:

  • Запретить распространять распространенные, безопасные, часто используемые идиомы программирования.
  • Сделайте дорогостоящий анализ всей программы, который заставляет компиляцию занимать часы, чтобы искать ошибки, которых, вероятно, нет.
  • Положитесь на автоматическую инициализацию на значения по умолчанию.

Команда разработчиков выбрала третий вариант.

Ответ 2

Когда я создаю тот же bool в моем методе, bool check (вместо внутри класса), я получаю сообщение об ошибке "использование неназначенной локальной переменной проверьте". Почему?

Потому что компилятор пытается помешать вам совершить ошибку.

Инициализирует ли переменная переменную false что-либо в этом конкретном пути выполнения? Вероятно, нет, учитывая, что default(bool) является ложным в любом случае, но это заставляет вас знать, что это происходит. Среда .NET препятствует доступу к "мусорной памяти", поскольку она инициализирует любое значение по умолчанию. Но все же, представьте, что это ссылочный тип, и вы передадите неинициализированное (нулевое) значение методу, ожидающему ненулевое, и получите NRE во время выполнения. Компилятор просто пытается предотвратить это, принимая тот факт, что это может иногда приводить к операторам bool b = false.

Эрик Липперт говорит об этом в сообщении в блоге:

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

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

Ответ 3

Почему локальные переменные требуют инициализации, но полей нет?

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

Почему локальные переменные требуют инициализации?

Это не более чем дизайнерское решение языка С#, как объясняемое Эриком Липпертом. CLR и среда .NET не требуют этого. Например, VB.NET будет компилировать только отлично с неинициализированными локальными переменными, и на самом деле CLR инициализирует все неинициализированные переменные значениями по умолчанию.

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

Почему поля не требуют инициализации?

Итак, почему эта принудительная явная инициализация не происходит с полями внутри класса? Просто потому, что эта явная инициализация может возникать во время построения, через свойство, вызываемое инициализатором объекта, или даже методом, называемым долго после события. Компилятор не может использовать статический анализ, чтобы определить, все ли возможные пути через код приводят к явной инициализации переменной перед нами. Неправильное поведение будет раздражать, так как разработчику может быть оставлен действующий код, который не будет компилироваться. Таким образом, С# не применяет его вообще, и CLR остается автоматически инициализировать поля до значения по умолчанию, если явно не задано.

Как насчет типов коллекций?

С# принудительное использование локальной инициализации переменных ограничено, что часто вылавливает разработчиков. Рассмотрим следующие четыре строки кода:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

Вторая строка кода не будет компилироваться, поскольку она пытается прочитать неинициализированную строковую переменную. Четвертая строка кода компилируется просто отлично, поскольку array был инициализирован, но только со значениями по умолчанию. Поскольку значение по умолчанию для строки равно null, мы получаем исключение во время выполнения. Любой, кто потратил время на Stack Overflow, будет знать, что эта явная/неявная несогласованность инициализации приводит к большому количеству "Почему я получаю ссылку на объект, не установленную на экземпляр объекта"? вопросы.

Ответ 4

Хорошие ответы выше, но я думал, что опубликую гораздо более простой/короткий ответ, чтобы люди ленились читать длинный (например, я).

Класс

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

Свойство Boo может быть или не быть инициализировано в конструкторе. Поэтому, когда он находит return Boo;, он не предполагает, что он был инициализирован. Он просто подавляет ошибку.

Функции

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

Символы { } определяют область действия блока кода. Компилятор просматривает ветки этих блоков { }, отслеживая материал. Легко сказать, что Boo не был инициализирован. Затем запускается ошибка.

Почему существует ошибка?

Ошибка была введена для уменьшения количества строк кода, необходимых для обеспечения безопасного кода. Без ошибки выше будет выглядеть так.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Из руководства:

Компилятор С# не позволяет использовать неинициализированные переменные. Если компилятор обнаруживает использование переменной, которая, возможно, не была инициализирована, генерирует ошибку компилятора CS0165. Для получения дополнительной информации см. Поля (Руководство по программированию на С#). Обратите внимание, что эта ошибка возникает, когда компилятор обнаруживает конструкцию, которая может привести к использованию неназначенной переменной, даже если ваш конкретный код этого не делает. Это позволяет избежать необходимости чрезмерно сложных правил для определенного назначения.

Ссылка: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx