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

141

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

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

начимэ
источник
Комментарии не предназначены для расширенного обсуждения; этот разговор был перемещен в чат .
Мартейн Питерс
14
Вопрос расплывчатый. Было бы приемлемым ответом «потому что так сказано в спецификации»?
Эрик Липперт,
4
Потому что именно так это было сделано на Java, когда они его скопировали. : P
Элвин Томпсон

Ответы:

178

Ответы Юваля и Дэвида в основном верны; резюмируя:

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

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

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

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

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

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

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

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

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

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

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

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

// 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() { } 
}

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

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

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

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

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

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

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

Эрик Липперт
источник
1
Отличный ответ, как всегда. Однако у меня есть вопрос: почему бы не назначить автоматически значения по умолчанию и локальным переменным? Другими словами, почему бы не сделать bool x;эквивалентным bool x = false; даже внутри метода ?
durron597
8
@ durron597: поскольку опыт показал, что забвение присвоить значение локальному, вероятно, является ошибкой. Если это, вероятно, ошибка, и ее дешево и легко обнаружить, тогда есть хороший стимул сделать поведение незаконным или предупредить.
Эрик Липперт,
27

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

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

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

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

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

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

Юваль Ицчаков
источник
«представьте, что это ссылочный тип, и вы передадите этот неинициализированный объект методу, ожидающему инициализированный». «Возможно, вы имели в виду:» представьте, что это ссылочный тип, и вы передали значение по умолчанию (null) вместо ссылки на объект "?
Дедупликатор
@ Дедупликатор Да. Метод, ожидающий ненулевого значения. Отредактировал эту часть. Надеюсь, теперь стало яснее.
Юваль Ицчаков
Не думаю, что это из-за проведенной линии. Каждый класс предполагает наличие конструктора, по крайней мере, конструктора по умолчанию. Поэтому, если вы придерживаетесь конструктора по умолчанию, вы получите значения по умолчанию (тихо прозрачные). При определении конструктора ожидается или предполагается, что вы знаете, что вы делаете в нем, и какие поля вы хотите инициализировать, и каким образом, включая знание значений по умолчанию.
Питер
Напротив: поле внутри метода может быть объявлено и присвоено значения на разных путях выполнения. Могут быть исключения, которые легко контролировать, пока вы не заглянете в документацию фреймворка, который вы можете использовать, или даже в других частях кода, которые вы не можете поддерживать. Это может привести к очень сложному исполнению. Поэтому составители намекают.
Питер
@Peter Я не совсем понял твой второй комментарий. Что касается первого, нет необходимости инициализировать какие-либо поля внутри конструктора. Это обычная практика . Задача компиляторов - не навязывать такую ​​практику. Вы не можете полагаться на какую-либо реализацию работающего конструктора и говорить «хорошо, все поля в порядке». Эрик подробно рассказал в своем ответе о способах инициализации поля класса и показывает, что вычисление всех логических способов инициализации занимает очень много времени .
Yuval Itzchakov
25

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

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

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

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

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

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

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

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

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

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

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

Дэвид Арно
источник
«Компилятор не может использовать статический анализ, чтобы определить, каждый ли возможный путь через код приводит к явной инициализации переменной перед нами». Я не уверен, что это правда. Можете выложить пример программы, устойчивой к статическому анализу?
Джон Кугельман
@JohnKugelman, рассмотрим простой случай public interface I1 { string str {get;set;} }и метод int f(I1 value) { return value.str.Length; }. Если это существует в библиотеке, компилятор не может знать, с чем будет связана эта библиотека, таким образом, было ли вызвано setзавещание до getполя поддержки, может быть явно не инициализировано, но он должен скомпилировать такой код.
Дэвид Арно,
Это правда, но я не ожидал, что при компиляции возникнет ошибка f. Он будет сгенерирован при компиляции конструкторов. Если вы оставите конструктор с полем, возможно, неинициализированным, это будет ошибкой. Также могут потребоваться ограничения на вызов методов и геттеров класса до инициализации всех полей.
Джон Кугельман,
@JohnKugelman: Я отправлю ответ, в котором будет обсуждаться поднятый вами вопрос.
Эрик Липперт,
4
Это не справедливо. Мы пытаемся поспорить здесь!
Джон Кугельман
11

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

Класс

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;
}

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

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

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

Reactgular
источник