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

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

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

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

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

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


Сообщение об ошибке:

Use of unassigned local variable 'test'

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


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

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

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

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

На полях: это еще интереснее. Рассмотрим тот же пример на C++:

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

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


person Spook    schedule 08.01.2013    source источник
comment
Компилятор не будет считать, что строка, присваивающая test, достигнута. Я не знаю, связано ли это с базовым анализом потока кода, который не следует за вызовами методов, или это связано с тем, как закрытые над локальными переменными превращаются в члены класса, сгенерированные кодом, или, возможно, и то, и другое, или ни то, ни другое. Но в любом случае инициализируйте переменную значением false при объявлении.   -  person Anthony Pegram    schedule 09.01.2013
comment
Я знаю это. Мой вопрос: почему компилятор не будет считать, что test присваивание достигнуто, когда на пути нет условных выражений?   -  person Spook    schedule 09.01.2013
comment
-1 даже за отсутствие сообщения об ошибке.   -  person Servy    schedule 09.01.2013
comment
@Spook Почему он должен предполагать, что так и будет? Алгоритм определения того, что переменная определенно назначена, не идеален по своей сути. Чтобы быть совершенным, было бы чрезвычайно сложно написать код для разработчиков компилятора, а также потребовалось бы чрезвычайно много времени, а компилятор предназначен для довольно быстрой компиляции кода. Это категория назначения, которую он предпочитает не поддерживать. Это не ошибка, поскольку они не забыли об этом.   -  person Servy    schedule 09.01.2013
comment
@Servy и еще один -1 за четкое указание на то, что вы могли опубликовать сообщение об ошибке (guess why), но вместо этого решили написать свой собственный небольшой мистический роман о стеке.   -  person Earlz    schedule 09.01.2013
comment
@Servy, на С++ я бы согласился, но на С#? Рассмотрено назначение переменной в условных ветвях. Передача в качестве параметра out всегда устанавливает переменную, передача в качестве ref требует инициализации переменной в любом случае. Помимо инициализации внутри делегата (ба, это должно быть лямбда, иначе у него не будет доступа к локальным переменным метода) - как я представил - какие есть другие варианты? Я не могу представить другого :)   -  person Spook    schedule 09.01.2013
comment
@Spook, почему ты не можешь создать замыкание с синтаксисом delegate? Вам не нужно использовать лямбду, не так ли?   -  person Chris Farmer    schedule 09.01.2013
comment
@Spook Во-первых, нет, это не обязательно должна быть лямбда, это должно быть замыкание, которое также может быть анонимным делегатом (delegate { test = true; };). Во-вторых, знать, что локальная переменная присваивается внутри делегата, несложно. На самом деле, чтобы поддерживать семантику замыкания, необходимо уже знать, что локальный объект доступен в замыкании, и поднять его. Сложный вопрос заключается в том, чтобы определить, выполняется ли этот делегат в какой-то момент, и пометить переменную как имеющую определенное значение в этот момент. В общем случае это нетривиальная задача.   -  person Servy    schedule 09.01.2013
comment
Этот случай описан (почти точно так же) в 12.3. 3.27 спецификации C#.   -  person Austin Salonen    schedule 09.01.2013


Ответы (4)


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

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

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

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

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

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

  • Состояние определенного присваивания внешней переменной v после expr такое же, как и состояние v до expr.

Пример

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.

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

person Anthony Pegram    schedule 08.01.2013
comment
И это именно то, о чем я просил. Спасибо! (любопытно, что в С++ эти правила немного отличаются) - person Spook; 09.01.2013
comment
@Spook: в C ++ использование неинициализированной переменной не является ошибкой времени компиляции, хотя во время выполнения вы получите неопределенное поведение. Предупреждение, которое вы получаете, это просто хороший компилятор; он, конечно, не будет пытаться выяснить это в каждом случае. - person GManNickG; 09.01.2013
comment
Хорошо, но когда я удаляю комментарий из лямбда-блока кода, компилятор перестает сообщать о том, что переменная не назначена - другими словами, условия для пометки переменной как используемой, когда она не назначена, больше не выполняются. По крайней мере, так это выглядит. - person Spook; 09.01.2013

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

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

person Toni Petrina    schedule 08.01.2013
comment
Мой фактический вопрос не в том, почему код не компилируется, а в том, почему компилятор не может сказать, что эта переменная назначена, несмотря на отсутствие условных операторов? - person Spook; 09.01.2013
comment
-1: локальная переменная присваивается до того, как она будет использована, компилятор слишком туп, чтобы понять это. - person Chris Dodd; 09.01.2013
comment
@ChrisDodd: Хотя ответ неверен, говоря, что он не может вывести его, вы так же ошибаетесь, говоря, что он слишком запутан, упрощая проблему. Это решаемая проблема, но в целом нетривиальная. - person GManNickG; 09.01.2013
comment
@GManNickG: Лучший способ охарактеризовать ситуацию: (1) общая проблема определения определенного присваивания неразрешима, поскольку она эквивалентна проблеме остановки, (2) из-за этого спецификация C # строго определяет определенное присваивание таким образом, что иногда сообщается о том, что переменная определенно не назначена, когда переменная на самом деле определенно назначена, и (3) в этом конкретном случае может работать межпроцедурный алгоритм отслеживания потока, но он не будет согласовываться с указанным поведением. - person Eric Lippert; 09.01.2013

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

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

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

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

person Servy    schedule 08.01.2013

Фрагмент из раздела 8.3 «Переменные и параметры» стандарта ECMA:

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

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

    }
}

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

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

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

person Brad Semrad    schedule 08.01.2013
comment
Чтобы это был достойный ответ, вам нужно сделать больше, чем сказать, что спецификация говорит, что она определенно не назначена со ссылкой на спецификации ... Что делает этот случай достойным обсуждения, так это то, что переменная определенно назначена, компилятор просто не могу сказать из-за сложности примера. - person Servy; 09.01.2013
comment
Итак, во-первых, все, что было до последнего абзаца и ссылки, бесполезно и должно быть просто удалено. Очевидно, что в ОП нет ничего нового. Далее, пока вы начинаете обсуждать проблему, это не совсем ответ, это просто напрашивается вопрос, почему компилятор не может знать, что вызов делегата, который определенно выполняется, и определенно назначается локальный, приводит к местный определенно назначается. Это действительно не очень ответ. - person Servy; 09.01.2013
comment
Этот случай описан (почти точно так же) в 12.3. 3.27 спецификации C#. - person Austin Salonen; 09.01.2013
comment
@AustinSalonen Да, не видел этого, когда смотрел спецификацию. Спасибо, что указали на это. - person Brad Semrad; 09.01.2013