Подтвердить что ты не робот

Лямбда, определяющая локальные переменные

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

static void Main(string[] args)
{
    bool test;

    Action lambda = () => { test = true; };
    lambda();

    if (test)
        Console.WriteLine("Ok.");
}

Он должен скомпилировать, правильно? Ну, это не так. Мой вопрос: согласно стандарту С#, должен ли этот код компилироваться или это ошибка компилятора?


Сообщение об ошибке:
Use of unassigned local variable 'test'

Примечание: Я знаю, как исправить ошибку, и я частично знаю, почему это происходит. Однако локальная переменная назначается безоговорочно, и я предполагаю, что компилятор должен это заметить, но это не так. Интересно, почему.


Комментарий к ответам: С# позволяет объявлять неназначенные переменные и это действительно полезно, т.е.

bool cond1, cond2;
if (someConditions)
{
    cond1 = someOtherConditions1;
    cond2 = someOtherConditions2;
}
else
{
    cond1 = someOtherConditions3;
    cond2 = someOtherConditions4;
}

Компилятор правильно компилирует этот код, и я думаю, что оставшиеся переменные неназначенные фактически делают код немного лучше, потому что:

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

На полях: Это еще более интересно. Рассмотрим тот же пример в С++:

int main(int argc, char * argv[])
{
    bool test;

    /* Comment or un-comment this block
    auto lambda = [&]() { test = true; };
    lambda();
    */

    if (test)
        printf("Ok.");

    return 0;
}

Если вы прокомментируете блокировку, компиляция заканчивается предупреждением:

main.cpp(12): warning C4700: uninitialized local variable 'test' used

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

4b9b3361

Ответ 1

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

Это не ошибка.

Раздел 5.3.3.29 Спецификация языка С# (4.0) описывает определенные правила назначения анонимных функций, в том числе лямбда-выражения. Я отправлю его здесь.

5.3.3.29 Анонимные функции

Для выражения лямбда-выражения или выражения анонимного метода expr с телом (либо блочным, либо выражение) body:

  • Определенное состояние присваивания внешней переменной v перед тело совпадает с состоянием v перед выражением. То есть определенная присваивание внешних переменных наследуется из контекста анонимная функция.

  • Определенное состояние присваивания внешней переменной v после expr совпадает с состоянием v перед выражением.

Пример

delegate bool Filter(int i);

void F() {
    int max;

    // Error, max is not definitely assigned    
    Filter f = (int n) => n < max;

    max = 5;    
    DoWork(f); 
}

генерирует ошибку времени компиляции, так как max определенно не назначен где анонимная функция объявлена. Пример

delegate void D();

void F() {    
    int n;    
    D d = () => { n = 1; };

    d();

    // Error, n is not definitely assigned
    Console.WriteLine(n); 
}

также генерирует ошибку времени компиляции, поскольку присвоение n в анонимная функция не влияет на определенное состояние присвоения n вне анонимной функции.

Вы можете видеть, как это относится к вашему конкретному примеру. Переменная test специально не назначается перед объявлением лямбда-выражения. Он не назначается специально до выполнения лямбда-выражения. И это не назначается специально после завершения выполнения лямбда-выражения. По правилу компилятор не считает, что переменная определенно назначается в точке ее чтения в инструкции if.

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

Ответ 2

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

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

Ответ 3

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

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

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

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

Ответ 4

Отрывок из раздела Стандарт ECMA 8.3 Переменные и параметры:

Переменная должна быть назначена до того, как ее значение будет получено. Пример

class Test
{
    static void Main() {
    int a;
    int b = 1;
    int c = a + b; // error, a not yet assigned

    }
}

приводит к ошибке времени компиляции, поскольку она пытается использовать переменную a до того, как ей будет присвоено значение. правила, регулирующие определенное задание, определены в §12.3.

Поэтому он заявляет, что переменная должна быть назначена до ее использования, иначе она приведет к ошибке компилятора. Поскольку вы создаете делегат и вызываете его, метод, содержащийся в вызове делегата, технически неизвестен. Поэтому компилятор не должен был это понять. Помните, что это метод делегата Invoke, который вызывается не фактическим методом.

Стандарт ECMA для С#