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'

是什么原因导致此问题?是否可以通过编译器设置修复它?


11
@BinaryWorrier:为什么?仅b在通过out参数分配后使用。
乔恩·斯基特

1
VS 2015文档说: “尽管以out参数形式传递的变量不必在传递之前初始化,但要求被调用的方法在方法返回之前分配一个值。” 所以这看起来确实像是个错误,是的,可以保证由tryParse进行初始化。
Rup

3
不管错误如何,此代码都充分说明了有关out参数的所有问题。那会TryParse返回一个可空值(或等效值)。
康拉德·鲁道夫2015年

1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= b看起来
Rawling

2
请注意,您可以将其简化为decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;。我已经打开了罗斯林(Roslyn)的bug来提出这个问题。
罗林

Answers:


112

是什么导致此问题?

对我来说似乎是一个编译器错误。至少,确实如此。虽然decimal.TryParse(v, out a)decimal.TryParse(v, out b)表现进行动态评估,我希望编译器还了解到,通过它到达的时间a <= b,无论是ab肯定分配。即使您在动态类型输入中可能会遇到一些怪异问题,但我希望仅a <= b在评估两个TryParse调用之后才进行评估。

然而,事实证明,通过运营商和转换棘手的,这是完全可行的有一个表达A && B && C其评估AC而不是B-如果你够狡猾。有关Neal Gafter的巧妙示例,请参阅Roslyn错误报告

使其工作dynamic更加困难-操作数动态时涉及的语义更难描述,因为要执行重载解析,您需要评估操作数以找出涉及的类型,这可能是违反直觉的。但是,Neal再次提出了一个示例,该示例表明需要编译器错误...这不是错误,而是错误修复。尼尔(Neal)证明了这一点非常感谢。

是否可以通过编译器设置修复它?

否,但是有其他选择可以避免该错误。

首先,您可以阻止它动态化-如果您知道只使用字符串,则可以使用IEnumerable<string> 给range变量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;可能会消除该错误,但由于尚未计算out值,因此a <= b将始终使用0m
Paw Baltzersen

12
@PawBaltzersen:是什么让你觉得呢?它总是在比较之前分配-只是出于某种原因(基本上是一个错误),编译器无法证明它。
乔恩·斯基特

1
具有无副作用的解析方法,即 decimal? TryParseDecimal(string txt)可能也是一个解决方案
zahir 2015年

1
我想知道这是否是懒惰的初始化;它认为“如果第一个是正确的,那么我就不需要评估第二个,这意味着b可能没有分配”;我知道这是无效的推论,但它解释了为什么括号可以解决问题……
durron597


16

由于我在错误报告中受了如此重的教育,因此我将尝试自己解释一下。


试想一下,T是一种用户定义的类型,其隐式转换为boolfalse和之间交替true,以开头false。据编译器所知,第dynamic一个参数的第一个参数&&可能会评估为该类型,因此它必须是悲观的。

如果然后让代码进行编译,则可能会发生这种情况:

  • 当动态活页夹评估第一个时&&,它将执行以下操作:
    • 评估第一个论点
    • 它是T-隐式转换为bool
    • 哦,是false,所以我们不需要评估第二个参数。
    • &&评估结果作为第一个参数。(不,不是false,由于某种原因。)
  • 当动态活页夹评估第二个时&&,它将执行以下操作:
    • 评估第一个论点。
    • 它是T-隐式转换为bool
    • 哦,是true,所以评估第二个参数。
    • ...哦,废话,b没有分配。

简而言之,用规范术语来说,有一些特殊的“确定赋值”规则,这些规则不仅让我们说变量是“确定赋值”还是“不确定赋值”,而且还说变量是“在false语句后确定赋值”还是“确定赋值” true声明后分配”。

它们存在,以便在处理和&&||(以及!???:)时,编译器可以检查是否可以在复杂的布尔表达式的特定分支中分配变量。

但是,这些仅在表达式的类型为boolean时起作用。当表达式的一部分为dynamic(或非布尔静态类型)时,我们将无法再可靠地说该表达式为truefalse-下次将其强制转换bool为决定采用哪个分支时,它可能已经改变了主意。


更新:现在已解决记录了问题

以前的编译器为动态表达式实现的确定分配规则允许某些情况下的代码可能导致读取未明确分配的变量。有关此内容的一份报告,请参见https://github.com/dotnet/roslyn/issues/4509

...

由于这种可能性,如果val没有初始值,则编译器不得允许编译该程序。即使val没有初始值,编译器的早期版本(VS2015之前的版本)也允许该程序进行编译。Roslyn现在诊断这种尝试读取可能未初始化的变量的尝试。


1
我在另一台计算机上使用VS2013,实际上已经设法使用此方法读取未分配的内存。这不是很令人兴奋:(
Rawling

您可以使用简单的委托读取未初始化的变量。创建一个委托out到具有的方法ref。它将很高兴地做到这一点,并且将在不更改值的情况下分配变量。
IllidanS4希望莫妮卡回到2015年

出于好奇,我使用C#v4测试了该代码段。但是出于好奇-编译器如何决定使用运算符false/ true而不是隐式强制转换运算符?在本地,它将调用implicit operator bool第一个参数,然后调用第二个操作数,调用operator false第一个操作数,然后再次调用第一个操作implicit operator bool数。这对我来说没有意义,基本上第一个操作数应该简化为布尔值一次,不是吗?
罗布

@Rob这是dynamic连锁&&情况吗?我已经看到它基本上可以使用(1)评估第一个参数(2)使用隐式强制转换来查看是否可以短路(3)我不能短路,因此评估第二个参数(4)现在我知道这两种类型,可以看到最好的&&是一个用户定义的&(5)调用操作符false上的第一个参数,以查看是否余能短路(6)我可以(因为falseimplicit bool不同意),所以结果是第一个参数...然后下一&&,(7)使用隐式强制转换来查看是否可以再次短路。
罗林

@ IllidanS4听起来很有趣,但是我还没有发现如何做。可以给我摘录吗?
罗林

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.