Рослін не вдалося скомпілювати код


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'

Що спричиняє цю проблему? Чи можна це виправити за допомогою налаштування компілятора?


11
@BinaryWorrier: Чому? Він використовує лише bпісля присвоєння його за допомогою outпараметра.
Джон Скіт,

1
У документації VS 2015 сказано: "Хоча змінні, передані як аргументи, не повинні бути ініціалізовані перед передачею, викликаний метод необхідний для присвоєння значення до повернення методу." так що це виглядає як помилка так, це гарантовано ініціалізується цим tryParse.
Руп

3
Незалежно від помилки, цей код ілюструє все погане в outаргументах. Якщо б це TryParseповернуло анульоване значення (або еквівалент).
Конрад Рудольф

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, яка піднімає це.
Роулінг

Відповіді:


112

Що спричиняє цю проблему?

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

Тим НЕ менше, виявляється, що через оператор і перетворення складного, це цілком можливо , щоб мати вираз , A && B && Cяке наводиться Aі , Cале не B- якщо ви хитра достатньо. Дивіться звіт про помилки Росліна для геніального прикладу Ніла Гафтера.

Зробити цю роботу 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
using decimal a, b = 0m;може видалити помилку, але тоді a <= bбуде використовувати завжди 0m, оскільки вихідне значення ще не розраховане.
Paw Baltzersen

12
@PawBaltzersen: Що змушує вас це думати? Він завжди буде призначений перед порівнянням - просто компілятор чомусь не може це довести (помилка, в основному).
Джон Скіт,

1
Наявність методу синтаксичного аналізу без побічного ефекту, тобто decimal? TryParseDecimal(string txt)може бути і рішенням
zahir

1
Цікаво, чи це лінива ініціалізація; він думає, "якщо перший з них є істинним, то мені не потрібно оцінювати другий, що означає, що він bможе не бути призначений"; Я знаю, що це недійсні міркування, але це пояснює, чому в дужках це виправлено ...
durron597

21

Здається, це помилка або принаймні регресія у компіляторі Roslyn. Для його відстеження було зареєстровано таку помилку:

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

Тим часом у чудової відповіді Джона є кілька робочих шляхів.


Також ця помилка, яка зазначає, що це зараз не пов’язано з LINQ ...
Роулінг

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 не має початкового значення. Зараз Рослін діагностує цю спробу прочитати можливо неініціалізовану змінну.


1
Використовуючи VS2013 на моїй іншій машині, я насправді встиг прочитати неназначену пам’ять, використовуючи це. Це не дуже захоплююче :(
Роулінг

Ви можете читати неініціалізовані змінні за допомогою простого делегата. Створіть делегата, який потрапляє outдо методу, який має ref. Він із задоволенням зробить це і зробить призначені змінні, не змінюючи значення.
IllidanS4 хоче повернути Моніку

З цікавості я протестував цей фрагмент на C # v4. Однак з цікавості - як компілятор вирішує використовувати оператор false/ trueна відміну від неявного оператора приведення? Локально він буде викликати implicit operator boolперший аргумент, потім викликати другий операнд, викликати operator falseперший операнд, після чого зновуimplicit operator bool перший операнд . Для мене це не має сенсу, перший операнд повинен, по суті, звестись до булевого значення один раз, ні?
Роб

@Rob Це той dynamic, прикутий &&випадок? Я бачив, що це в основному go (1) оцінює перший аргумент (2) використовує неявний привід, щоб побачити, чи можу я коротке замикання (3) Не можу, тому ухиляюся від другого аргументу (4) тепер я знаю обидва типи, я можна побачити найкраще &&- це визначений користувачем &(5) оператор виклику falseза першим аргументом, щоб побачити, чи можу я коротке замикання (6) я можу (тому що falseі implicit boolне згоден), тому результат є першим аргументом ... а потім наступним &&, (7) використовуйте неявний привід, щоб побачити, чи зможу я коротке замикання (знову).
Роулінг

@ IllidanS4 Звучить цікаво, але я не дізнався, як це зробити. Чи можете ви дати мені фрагмент?
Роулінг

15

Це не помилка. Дивіться https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 для прикладу того, як динамічний вираз цієї форми може залишити таку вихідну змінну без призначення.


1
Оскільки моя відповідь прийнята і високо оцінена, я відредагував її, щоб вказати рішення. Дякую за всю вашу роботу над цим, включаючи пояснення мені моєї помилки :)
Джон Скіт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.