非shortcircuting布尔运算符和C#7模式匹配

时间:2017-08-24 16:27:17

标签: c# c#-7.0

我目前正在编写一个针对.NET 4.7(C#7)的C#应用​​程序。在我尝试使用" is"来声明变量的新方法后,我很困惑。关键词:     if (variable is MyClass classInstance) 这种方式有效,但在做的时候:

if (true & variable is MyClass classInstance)
{
    var a = classInstance;
}

Visual Studio(我使用2017年)向我显示错误Use of unassigned local variable 'classInstance'。使用&&&)的短路版本,它可以正常工作。我错过了关于&运算符的内容吗? (我知道使用shortcircuting版本更常用,但此时我只是好奇)

3 个答案:

答案 0 :(得分:10)

这个伤了我的脑袋,但我想我已经明白了。

这种混淆是由两个怪癖引起的:is操作离开变量unclared(非null)的方式,以及编译器优化掉布尔运算但不是按位运算的方式。

Quirk 1.如果强制转换失败,则变量未分配(非空)

根据documentation for the new is syntax

  

如果exp为true且与if语句一起使用,则 varname被分配并且仅在if语句中具有本地范围。

如果您在两行之间阅读,则表示如果is投射失败,则该变量被视为未分配。这可能违反直觉(有些人可能会认为它是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.布尔运算已经过优化,二进制运算不是

当编译器检测到预定义的布尔结果时,编译器将不会发出无法访问的代码,并因此跳过某些验证。例如:

编译:

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编译器显然执行与它相同的验证。这可能看起来有点奇怪,但请记住,当您使用&而不是&&时,可以保证&必须执行,尽管在此示例中它似乎并不重要,一般情况必须允许其他复杂因素,例如运算符重载。

怪癖如何结合

在最后一个示例中,if子句的结果是在运行时确定的,而不是在编译时确定的。因此,在执行y块的内容之前,编译器无法确定if将最终被分配。因此你得到了

    if (true & instance is MyClass y)
    {
        var x = y; //Error: use of unassigned local variable
    }

<强> TLDR

在复合逻辑运算的情况下,当且仅当转换成功时,c#无法确定整个if条件将解析为true。如果不确定,它就不能允许访问变量,因为它可能是未分配的。在编译时可以将表达式简化为非复合操作,例如通过删除true &&,就会出现异常。

解决方法

我认为我们使用新is语法的方式是一个带有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
        }
    }
}

答案 1 :(得分:1)

  

我错过了关于&amp;操作

不,我不这么认为。你的期望对我来说似乎是对的。考虑这个替代示例,它编译时没有错误:

object variable = null;
MyClass classInstance;

if (true & ((classInstance = variable as MyClass) != null))
{
    var a = classInstance;
}

var b = classInstance;

(对我来说,考虑在<{1}}身体外的分配更有意思,因为短路会影响行为。)

通过显式分配,编译器会在ifclassInstance的分配中将a识别为明确分配。它应该能够用新语法做同样的事情。

对于逻辑b,短路与否应该不重要。您的第一个值是and,因此应始终需要对后半部分进行求值以获得整个表达式。正如您所注意到的,编译器确实以不同方式处理true&,这是意料之外的。

此代码的变体是:

&&

编译器正确地将static void M3() { object variable = null; if (true | variable is MyClass classInstance) { var a = classInstance; } } 标识为在使用classInstance时未明确分配,但与||具有相同的明显不当行为(即也说明|并非绝对虽然使用非短路操作员,但无论如何都必须进行分配。

同样,上面的分配工作正确,而不是使用新语法。

如果只是关于未使用新语法解决的明确分配规则,那么我希望classInstance&&一样破碎。但事实并非如此。编译器正确处理。事实上,在the feature documentation中(我犹豫地说&#34;规范&#34;,因为还没有ECMA批准的C#7规范),它的内容如下:

  

type_pattern测试表达式是否为给定类型,并在测试成功时将其强制转换为该类型。这引入了由给定标识符命名的给定类型的局部变量。 当模式匹配操作的结果为真时,明确赋予该局部变量。 [强调我的]

由于短路在没有模式匹配的情况下产生正确的行为,并且由于模式匹配产生了正确的行为而没有短路(并且在特征描述中明确指出了明确赋值),我会说这是编译器错误的直接行为。非短路布尔运算符之间可能存在一些被忽略的交互,以及评估模式匹配表达式导致明确赋值在混洗中丢失的方式。

您应该考虑向当局报告。我想现在,Roslyn GitHub issue-tracking是跟踪这类事情的地方。如果您在报告中解释如何找到这个以及为什么特定语法在您的场景中很重要(因为在您发布的代码中,&运算符等效地工作...非短路{{1似乎没有给代码带来任何好处。

答案 2 :(得分:1)

这是由于有明确规定的规则所致,&&有一种特殊情况,而没有&的一种特殊情况。我相信C#设计团队正在按照预期的要求工作,但至少在清晰度方面,该规范可能还有更多工作要做。

根据ECMA C#5标准,第5.3.3.24节:

  

对于expr-first && expr-second形式的表达式expr:

     

...

     

expr之后v的确定赋值状态由以下条件确定:

     
      
  • 如果expr-first是值为false的常数表达式,则expr之后的v的明确赋值状态与expr-first之后的v的明确赋值状态相同。
  •   
  • 否则,如果明确分配了expr-first之后的v的状态,那么肯定分配了expr之后的v的状态。
  •   
  • 否则,如果明确分配了expr-second之后的v的状态,并且“在错误表达式之后确定地分配了” expr-first之后的v的状态,则肯定分配了expr之后的v的状态。
  •   
  • 否则,如果对expr-second之后的v的状态进行了明确赋值或“在真表达式之后进行了绝对赋值”,则将expr之后的v的状态进行了“对真表达式之后进行绝对赋值”。 / li>   
  • ...
  •   

此案例的相关部分是我以粗体突出显示的部分。 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最终被“在真实表达式之后确定赋值”的表达式。同样,上述代码由于未明确分配x而无法编译,而如果将&更改为&&,则会编译。因此,至少在模式匹配本身中这不是问题。

令我有些困惑的是,由于5.3.3.21(“ 5.3.3.21带有嵌入式表达式的表达式的一般规则”),为什么x仍然没有“在真实表达式之后绝对赋值”,其中包含:

  
      
  • 在expr末尾的v的明确赋值状态与在expr n 末尾的确定性赋值状态相同。
  •   

我怀疑这是指仅包括“绝对分配”或“未绝对分配”,而不是包括“在真表达式后绝对分配”部分-尽管不够清晰。