Roslyn не удалось скомпилировать код

95

После того, как я перенес свой проект с VS2013 на VS2015, проект больше не строится. Ошибка компиляции возникает в следующем операторе LINQ:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

Компилятор возвращает ошибку:

Ошибка CS0165 Использование неназначенной локальной переменной 'b'

Что вызывает эту проблему? Можно ли это исправить настройкой компилятора?

ramil89
источник
11
@BinaryWorrier: Почему? Он используется только bпосле его назначения через outпараметр.
Джон Скит,
1
В документации VS 2015 говорится: «Хотя переменные, передаваемые в качестве исходящих аргументов, не должны быть инициализированы перед передачей, вызываемый метод должен присвоить значение перед возвратом метода». так что это действительно похоже на ошибку, да, она гарантированно будет инициализирована этим tryParse.
Rup
3
Независимо от ошибки, этот код продемонстрировал все плохое в outаргументах. Вернет ли это TryParseзначение, допускающее значение NULL (или эквивалентное).
Конрад Рудольф
1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bвыглядит намного лучше
Роулинг,
2
Обратите внимание: вы можете упростить это до decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Я обнаружил ошибку Roslyn, поднимающую это.
Rawling

Ответы:

112

Что вызывает эту проблему?

Мне кажется, это ошибка компилятора. По крайней мере, так оно и было. Хотя decimal.TryParse(v, out a)и decimal.TryParse(v, out b)выражения вычисляются динамически, я ожидал , что компилятор до сих пор понять , что к тому времени он достигает a <= b, как aи bопределенно присвоенной. Даже с учетом странностей, которые могут возникнуть при динамической типизации, я бы ожидал, что когда-либо буду оценивать только a <= bпосле оценки обоих TryParseвызовов.

Тем не менее, оказывается, что через оператор и преобразование сложного, это вполне возможно , чтобы иметь выражение , A && B && Cкоторое приводится Aи , Cно не B- если вы хитрая достаточно. См. Гениальный пример Нила Гафтера в отчете об ошибке Roslyn .

Заставить эту работу работать dynamicеще сложнее - семантику, используемую, когда операнды являются динамическими, сложнее описать, потому что для выполнения разрешения перегрузки вам необходимо оценить операнды, чтобы выяснить, какие типы задействованы, что может быть нелогичным. Однако, опять - таки Нил придумал пример , который показывает , что требуется ошибка компилятора ... Это не ошибка, это ошибка исправления . Большое спасибо Нилу за доказательство этого.

Можно ли исправить через настройки компилятора?

Нет, но есть альтернативы, позволяющие избежать ошибки.

Во-первых, вы можете сделать так, чтобы он не был динамическим - если вы знаете, что будете использовать только строки, вы можете использовать IEnumerable<string> или дать переменной диапазона vтип string(т.е. from string v in array). Это был бы мой предпочтительный вариант.

Если вам действительно нужно сохранить динамику, просто укажите bзначение для начала:

decimal a, b = 0m;

Это не принесет никакого вреда - мы знаем, что на самом деле ваша динамическая оценка не сделает ничего сумасшедшего, поэтому вы все равно будете присваивать значение, bпрежде чем использовать его, делая начальное значение несущественным.

Вдобавок кажется, что добавление круглых скобок тоже работает:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Это меняет точку, в которой запускаются различные части разрешения перегрузки, и делает компилятор счастливым.

Остается еще одна проблема - &&необходимо уточнить правила спецификации по определенному назначению с оператором, чтобы указать, что они применяются только тогда, когда &&оператор используется в его «обычной» реализации с двумя boolоперандами. Я постараюсь исправить это для следующего стандарта ECMA.

Джон Скит
источник
Да! У IEnumerable<string>меня сработало применение или добавление скобок. Теперь компилятор строит без ошибок.
ramil89
1
использование decimal a, b = 0m;может удалить ошибку, но тогда a <= bбудет использоваться всегда 0m, поскольку выходное значение еще не вычислено.
Paw Baltzersen
12
@PawBaltzersen: С чего вы так думаете? Он всегда будет присваиваться перед сравнением - просто компилятор по какой-то причине не может это доказать (по сути, это ошибка).
Джон Скит,
1
Наличие метода синтаксического анализа без побочного эффекта, т.е. decimal? TryParseDecimal(string txt)тоже может быть решением
захир
1
Интересно, это ленивая инициализация; он думает, что «если первое верно, то мне не нужно оценивать вторую, что означает, что она bможет не быть назначена»; Я знаю, что это неверное рассуждение, но оно объясняет, почему круглые скобки исправляют это ...
durron597
21

Это действительно похоже на ошибку или, по крайней мере, регресс в компиляторе Roslyn. Для его отслеживания была зарегистрирована следующая ошибка:

https://github.com/dotnet/roslyn/issues/4509

А пока у отличного ответа Джона есть пара обходных путей.

ДжаредПар
источник
Также эта ошибка, в которой отмечается, что она не связана с LINQ ...
Rawling,
16

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


Представьте, что Tэто некоторый определяемый пользователем тип с неявным приведением к, boolкоторое чередуется между falseи true, начиная с false. Насколько известно компилятору, dynamicпервый аргумент первого &&может оценивать этот тип, поэтому он должен быть пессимистичным.

Если тогда он позволит коду скомпилировать, это может произойти:

  • Когда динамическое связующее оценивает первое &&, оно выполняет следующие действия:
    • Оцените первый аргумент
    • Это T- неявно приведено к bool.
    • О, это так false, поэтому нам не нужно оценивать второй аргумент.
    • Сделайте результат &&оценки первым аргументом. (Нет, falseпо какой-то причине.)
  • Когда динамическое связующее оценивает второе &&, оно выполняет следующие действия:
    • Оцените первый аргумент.
    • Это T- неявно приведено к bool.
    • Ах да, trueтак что оцените второй аргумент.
    • ... Вот дерьмо, bне назначено.

Короче говоря, существуют специальные правила «определенного присваивания», которые позволяют нам говорить не только о том, является ли переменная «определенно назначенной» или «определенно не назначенной», но также и «определенно назначена после falseоператора» или «определенно назначена». назначается после trueзаявления ".

Они существуют для того, чтобы при работе с &&и ||!и ??и ?:) компилятор мог проверить, могут ли быть присвоены переменные в определенных ветвях сложного логического выражения.

Однако они работают, только если типы выражений остаются логическими . Когда часть выражения является dynamic(или не-логическим статическим типом), мы больше не можем достоверно сказать, что выражение является trueили false- в следующий раз, когда мы приведем его, boolчтобы решить, какую ветвь выбрать, оно могло передумать.


Обновление: теперь это решено и задокументировано :

Правила определенного присваивания, реализованные предыдущими компиляторами для динамических выражений, допускали некоторые случаи кода, которые могли привести к чтению переменных, которые не были назначены определенно. См. Https://github.com/dotnet/roslyn/issues/4509 для одного отчета об этом.

...

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

Роулинг
источник
1
Используя VS2013 на другом моем компьютере, мне действительно удалось прочитать неназначенную память, используя это. Это не очень интересно :(
Rawling
Вы можете читать неинициализированные переменные с помощью простого делегата. Создайте делегата, который получит доступ outк методу, имеющему ref. Он с радостью сделает это и назначит переменные, не изменяя значения.
IllidanS4 хочет вернуть Монику
Из любопытства я протестировал этот фрагмент на C # v4. Однако из любопытства - как компилятор решает использовать оператор false/ trueвместо неявного оператора приведения? Локально, он будет вызывать implicit operator boolна первый аргумент, затем вызвать второго операнда, вызов operator falseна первый операнд, а затем implicit operator boolна первый операнд снова . Для меня это не имеет смысла, первый операнд должен сводиться к логическому разу, не так ли?
Роб
@ Роб Это тот случай dynamic, связанный цепью &&? Я видел, как это в основном идет (1) оценивает первый аргумент (2) использует неявное приведение, чтобы увидеть, могу ли я закоротить (3) я не могу, поэтому оцените второй аргумент (4), теперь я знаю оба типа, я может видеть лучше &&это определяемый пользователем &(5) оператор вызова falseна первый аргумент , чтобы увидеть , если я могу короткого замыкания (6) я могу (потому falseи не implicit boolсогласен), так что результат является первым аргументом ... , а затем следующий &&, (7) используйте неявное приведение, чтобы увидеть, могу ли я (снова) закоротить.
Rawling
@ IllidanS4 Звучит интересно, но я не понял, как это сделать. Можете дать мне отрывок?
Роулинг,
15

Это не ошибка. См. Https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 для примера того, как динамическое выражение этой формы может оставить такую ​​выходную переменную неназначенной.

Нил Гафтер
источник
1
Поскольку мой ответ принят и получил высокую оценку, я отредактировал его, чтобы указать решение. Спасибо за всю вашу работу над этим - в том числе за объяснение моей ошибки :)
Джон Скит,