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();
}

编译器返回错误:

Error CS0165 使用未赋值的局部变量'b'

是什么原因导致了这个问题? 是否可以通过编译器设置来解决它?


11
为什么?它只是在通过 out 参数分配后才使用 b - Jon Skeet
1
VS 2015文档指出:"虽然作为out参数传递的变量在传递之前不必初始化,但被调用的方法在返回之前必须分配一个值。" 所以这似乎是一个bug,是由tryParse保证初始化的。 - Rup
3
不管错误是什么,这段代码展示了out参数的所有弊端。要是TryParse返回一个可为空的值(或等价物)就好了。 - Konrad Rudolph
1
@Rawling 当然不是,那段代码很愚蠢;但这并不是你实际编写的方式。你应该写成 from v in array let a = decimal.Parse(v) let b = decimal.Parse("15") where a <= b select v(或者,如果可空类型是合适的单子,你可以使用 SelectMany 并编写 from v in array from a in Parse(v) from b in Parse("15") where a <= b select v —— 这实际上是有效的 C# 代码,只要定义了合适的 Parse)。在真正的代码中(OP 的代码根本没有意义),这种写法甚至更易读。 - Konrad Rudolph
2
只是提醒一下,你可以简化为 decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;。我已经提交了一个Roslyn bug来解决这个问题。 - Rawling
显示剩余7条评论
4个回答

112

是什么导致了这个问题?

在我看来,这似乎是编译器的一个 bug。尽管 decimal.TryParse(v, out a)decimal.TryParse(v, out b) 表达式是动态评估的,但是我期望编译器在到达a <= b时仍能理解两个变量都已经被赋值。即使在动态类型中可以想出一些怪异的情况,我也希望只有在评估了两个TryParse调用之后才会评估a <= b

然而,通过操作和转换的技巧,完全有可能有一个表达式A && B && C,它评估了AC,但没有评估B - 如果你足够狡猾。请参阅Roslyn bug report中Neal Gafter聪明的示例。

要使其与dynamic一起工作更加困难-当操作数是动态的时所涉及的语义更难描述,因为为了执行重载解析,需要评估操作数以找出涉及哪些类型,这可能是直觉反感的。然而,Neal再次提供了一个示例,显示编译器错误是必需的...这不是一个 bug,而是一个 bug 修复。对Neal的巨大成就给予赞扬。

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

不行,但有一些避免错误的替代方法。

首先,您可以停止使用动态类型-如果您知道只会使用字符串,则可以使用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
你是如何想到这一点的?它始终会在比较之前被分配 - 只是编译器出于某些原因(基本上是一个错误)无法证明它。 - Jon Skeet
1
拥有一个没有副作用的解析方法,例如decimal? TryParseDecimal(string txt)也可能是一个解决方案。 - zahir
1
我想知道这是否是懒初始化;它认为“如果第一个条件为真,则无需评估第二个条件,这意味着b可能未被分配”;我知道这是无效的推理,但这解释了为什么加上括号就可以解决问题... - durron597
显示剩余9条评论

21

还有这个关于bug的问题(https://github.com/dotnet/roslyn/issues/4507),指出它与LINQ无关... - Rawling

16
自从我在错误报告中被彻底教育后,我要试着自己解释一下。
假设T是一种用户定义的类型,它具有到布尔值的隐式转换,可以在false和true之间交替,以false开头。就编译器而言,第一个&&的动态第一个参数可能会评估为该类型,因此它必须持悲观态度。
然后,如果它让代码编译,就会发生这种情况:
当动态绑定程序评估第一个&&时,它执行以下操作:
评估第一个参数
这是一个T - 将其隐式转换为bool。
哦,它是false,所以我们不需要评估第二个参数。
使&&的结果评估为第一个参数。(不,不是false,出于某种原因。)
当动态绑定程序评估第二个&&时,它执行以下操作:
评估第一个参数。
这是一个T - 将其隐式转换为bool。
哦,它是true,所以评估第二个参数。
噢,糟糕,b没有被分配。
简而言之,在规范术语中,有特殊的“明确赋值”规则,让我们不仅可以说变量是否“明确赋值”或“未明确赋值”,还可以说它是否“在false语句之后明确赋值”或“在true语句之后明确赋值”。
这些存在是为了当处理&&和||(以及!和??和?:)时,编译器可以检查变量是否可能在复杂布尔表达式的特定分支中分配。
但是,只有在表达式类型保持为布尔类型时才起作用。当表达式的一部分是dynamic(或非布尔静态类型)时,我们无法可靠地说出表达式是true还是false - 下次我们将其转换为bool以决定采取哪个分支时,它的想法可能已经改变了。
更新:此问题现已得到解决并记录在GitHub上,具体信息可参见文档
先前编译器为动态表达式实现的明确赋值规则允许某些代码,这些代码可能导致读取未确定赋值的变量。有一份报告详细说明了此情况,请查看此处
因为存在这种可能性,如果val没有初始值,则编译器不能编译此程序。之前的编译器(VS2015之前)即使val没有初始值也会编译此程序。Roslyn现在会检测此尝试读取可能未初始化变量的行为。

1
在我的另一台机器上使用VS2013,我实际上已经成功读取了未分配的内存。这并不是非常令人兴奋 :( - Rawling
您可以使用简单的委托读取未初始化的变量。创建一个委托,将“out”传递给具有“ref”的方法。它会愉快地执行,并使变量被分配,而不改变其值。 - IS4
出于好奇,我用C# v4测试了那个片段。不过,编译器是如何决定使用运算符false/true而不是隐式转换运算符的呢?在本地,它将在第一个参数上调用implicit operator bool,然后调用第二个操作数,在第一个操作数上调用operator false,然后再次调用第一个操作数上的implicit operator bool。这对我来说没有意义,第一个操作数应该只需要简化为布尔值一次,不是吗? - Rob
@IllidanS4 听起来很有趣,但我还没有找到如何做。 你能给我一个片段吗? - Rawling
@Rawling,可以在这里查看:https://github.com/IllidanS4/SharpUtils/blob/master/Reference.cs#L38。 - IS4
显示剩余2条评论

15

1
由于我的答案被接受并得到了高票,我已经编辑了它以表明解决方案。感谢您在此过程中的所有工作 - 包括向我解释我的错误 :) - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接