非短路布尔运算符和C#7模式匹配
我目前正在编写一个针对.NET 4.7(C#7)的C#应用程序。 我尝试使用“is”关键字声明变量的新方法后感到困惑: if (variable is MyClass classInstance)
这种方式if (variable is MyClass classInstance)
,但在执行时:
if (true & variable is MyClass classInstance) { var a = classInstance; }
Visual Studio(我正在使用2017)向我展示Use of unassigned local variable 'classInstance'
的错误Use of unassigned local variable 'classInstance'
。 使用短小版本的&
( &&
)它可以正常工作。 我错过了关于&
运营商的一些事情吗? (我知道使用shortcircuting版本更常用,但此时我只是好奇)
这个伤到了我的脑袋,但我想我已经弄清楚了。
这种混淆是由两个怪癖引起的: is
操作离开变量unclared(非null)的方式,以及编译器优化掉布尔运算但不是按位运算的方式。
Quirk 1.如果强制转换失败,则变量未分配(非空)
根据新文档的语法 :
如果exp为true且与if语句一起使用, 则分配varname并且仅在if语句中具有本地范围。
如果您在行之间读取,这意味着如果转换失败,则该变量被视为未分配 。 这可能违反直觉(有些人可能会认为它是null
)。 这意味着if
块中依赖于变量的任何代码都不会编译,如果整个if
子句有可能在没有类型匹配的情况下计算为true
。 所以举个例子
这编译:
if (instance is MyClass y) { var x = y; }
这编译:
if (true && instance is MyClass y) { var x = y; }
但这不是:
void Test(bool f) { if (f && instance is MyClass y) { var x = y; //Error: Use of unassigned local variable } }
Quirk 2.布尔运算被优化掉,而二进制运算则没有
当编译器检测到预定义的布尔结果时,编译器将不会发出无法访问的代码,并因此跳过某些validation。 例如:
这编译:
void Test(bool f) { object neverAssigned; if (false && f) { var x = neverAssigned; //OK (never executes) } }
但是如果你使用&
而不是&&
,它就不会编译:
void Test(bool f) { object neverAssigned; if (false & f) { var x = neverAssigned; //Error: Use of unassigned local variable } }
当编译器看到类似true &&
东西时,它只是完全忽略它。 从而
if (true && instance is MyClass y)
完全一样
if (instance is MyClass y)
但是这个
if (true & instance is MyClass y)
不一样。 编译器仍然需要发出执行&
操作的代码并在条件语句中使用其输出。 或者即使它没有,当前的C#7编译器显然执行与它相同的validation。 这可能看起来有点奇怪,但请记住,当你使用&
而不是&&
,可以保证&
必须执行,虽然在这个例子中似乎不重要,但一般情况下必须允许其他复杂因素,例如作为运算符重载。
怪癖是如何结合的
在最后一个示例中, if
子句的结果是在运行时确定的,而不是在编译时确定的。 因此,编译器无法确定在执行if
块的内容之前, y
将最终被分配。 因此,你得到
if (true & instance is MyClass y) { var x = y; //Error: use of unassigned local variable }
TLDR
在复合逻辑运算的情况下,当且仅当转换成功时,c#无法确定整体if
条件是否将解析为true。 如果不确定,它就不能允许访问变量,因为它可能是未分配的。 当表达式可以在编译时简化为非复合操作时,例如通过删除true &&
,会产生exception。
解决方法
我认为我们使用new语法的方式是使用if
子句作为单个条件。 在开头添加true &&
是有效的,因为编译器只是删除它。 但是,与新语法结合的任何其他内容都会在代码块运行时对新变量是否处于未分配状态产生歧义,并且编译器不能允许这样做。
解决方法当然是嵌套条件而不是组合它们:
不会工作:
void Test(bool f) { if (f & instance is MyClass y) { var x = y; //Error: Use of unassigned local variable } }
工作良好:
void Test(bool f) { if (f) { if (instance is MyClass y) { var x = y; //Works } } }
我错过了关于&运营商的一些事情吗?
不,我不这么认为。 你的期望对我来说似乎是对的。 考虑这个替代示例,它编译时没有错误:
object variable = null; MyClass classInstance; if (true & ((classInstance = variable as MyClass) != null)) { var a = classInstance; } var b = classInstance;
(对我而言,考虑if
体外的分配更有意思,因为短路会影响行为。)
通过显式赋值,编译器在a
和b
的赋值classInstance
识别为明确赋值。 它应该能够使用新语法执行相同的操作。
有逻辑and
,短路或不应该无关紧要。 您的第一个值为true
,因此应始终需要对后半部分进行求值以获得整个表达式。 正如您所注意到的,编译器确实对待&
和&&
不同,这是出乎意料的。
这个代码的变化是:
static void M3() { object variable = null; if (true | variable is MyClass classInstance) { var a = classInstance; } }
编译器正确地将classInstance
标识为||
时未明确赋值 使用,但与|
相同的明显不当行为 (即也说明没有明确分配classInstance
),即使使用非短路运算符,也必须进行分配。
同样,上述工作正确地分配是明确的,而不是使用新的语法。
如果这只是关于未使用新语法解决的明确赋值规则,那么我希望&&
与&
一样破碎。 但事实并非如此。 编译器正确处理。 事实上,在function文档中 (我犹豫地说“规范”,因为还没有ECMA批准的C#7规范),它的内容如下:
type_pattern都测试表达式是否为给定类型,并在测试成功时将其强制转换为该类型。 这引入了由给定标识符命名的给定类型的局部变量。 当模式匹配操作的结果为真时,明确赋予该局部变量。 [强调我的]
由于短路在没有模式匹配的情况下产生正确的行为,并且由于模式匹配产生了正确的行为而没有短路(并且在特征描述中明确地解决了明确赋值),我想说这是编译器错误的直接行为。 非短路布尔运算符之间可能存在一些被忽略的交互,以及评估模式匹配表达式的方式,导致明确赋值在混洗中丢失。
您应该考虑向当局报告。 我想现在, Roslyn GitHub问题跟踪是跟踪这类事情的地方。 如果您在报告中解释如何找到这个以及为什么特定语法在您的场景中很重要可能会有所帮助(因为在您发布的代码中, &&
运算符等效地工作…非短路并且似乎不会赋予它任何代码的优点)。
这是由于明确赋值的规则,它具有&&
的特殊情况,但没有特殊情况的&
。 我相信它的工作符合C#设计团队的预期 ,但规范可能还有一些工作要做,至少在清晰度方面。
根据ECMA C#5标准,第5.3.3.24节:
对于
expr-first && expr-second
forms的expr-first && expr-second
:…
expr之后v的明确赋值状态由下式确定:
- 如果expr-first是值为false的常量表达式,则expr之后的v的明确赋值状态与expr-first之后的v的明确赋值状态相同。
- 否则,如果明确赋值expr-first之后的v的状态,则明确赋值expr之后的v的状态。
- 否则,如果明确赋值expr-second之后的v的状态,并且expr-first之后的v的状态是“在false表达式之后明确赋值”,则明确赋值expr之后的v的状态。
- 否则,如果expr-second之后的v的状态被明确赋值或“在真实表达式之后明确赋值”,则expr之后的v的状态是“在真实表达之后明确赋值”。
- …
这个案例的相关部分是我用粗体突出显示的部分。 classInstance
是“在真实表达式后明确赋值”的模式(上面的expr-second
),并且没有一个早期的情况适用,因此条件结尾处的整体状态是“在真实表达式后明确赋值”。 这意味着在if
语句体内,它是明确分配的。
&
运营商没有等效条款。 虽然可能存在,但涉及的类型会很复杂 – 当与bool
表达式一起使用时,它只需要应用于&
运算符,而我认为大多数其他明确的赋值都不会以这种方式处理类型。
请注意,您不需要使用模式匹配来查看此内容。
考虑以下程序:
using System; class Program { static void Main() { bool a = false; bool x; bool y = true; if (true & (y && (x = a))) { Console.WriteLine(x); } } }
表达式y && (x = a)
是另一个,其中x
最终为“在真实表达后明确赋值”。 同样,上面的代码由于没有明确分配而无法编译,而如果你将&
更改为&&
则编译。 所以至少这不是模式匹配本身的问题。
令我感到困惑的是,由于5.3.3.21(“5.3.3.21具有嵌入式表达式的表达式的一般规则”), x
仍未“在真实表达后明确赋值”,其中包含:
- expr末尾的v的明确赋值状态与expr n末尾的明确赋值状态相同。
我怀疑这只是包含“明确分配”或“未明确分配”,而不是包括“明确分配后的真实表达”部分 – 尽管它不是应该如此清晰。