【问题标题】:What's compiler thinking about the switch-statement?编译器对 switch 语句的想法是什么?
【发布时间】:2013-03-07 20:32:44
【问题描述】:

再次受到-5 问题的启发!

我阅读了@Quartermeister 的[this comment] 并感到惊讶!

那么为什么会编译

switch(1) {
    case 2:
}

但事实并非如此。

int i;

switch(i=1) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

不是这个

switch(2) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

更新:

-5 问题变成了-3

【问题讨论】:

  • 有可能编译器可以优化出第一个,但不能删除第二个,因为赋值。
  • 我的猜测是 1 是编译时已知的常量表达式,让编译器优化整个开关,而 i=1 是非常量表达式(尽管编译器也知道产生一个特定的值)所以编译器试图保持开关。
  • 它与:int i=1; switch(i) { case 2: // Control cannot fall through from one case label ('case 1:') to another } 有什么不同,我认为这是显而易见的......
  • @evanmcdonnal:是的,它允许的。 switch (i = 1){case 1: Console.WriteLine("foo"); break;} 编译、运行和输出“foo”。
  • 由于您似乎对 switch 语句感兴趣,您可能想阅读以下内容:blogs.msdn.com/b/ericlippert/archive/2009/08/13/…

标签: c# switch-statement unreachable-code


【解决方案1】:

它们都不应该编译。 C# 规范要求 switch 部分至少有一个语句。解析器应该禁止它。

让我们忽略解析器允许空语句列表的事实;那不是相关的。规范说switch部分的末端不能有可到达的端点;这是相关的部分。

在您的最后一个示例中,switch 部分有一个可到达的端点:

void M(int x) { switch(2) { case 2: ; } }

所以它一定是一个错误。

如果你有:

void M(int x) { switch(x) { case 2: ; } }

那么编译器不知道 x 是否永远是 2。它保守地假设它可以,并说该部分有一个可到达的端点,因为 switch case 标签是可到达的。

如果你有

void M(int x) { switch(1) { case 2: ; } }

然后编译器可以推断端点不可达,因为案例标签不可达。编译器知道常量 1 永远不会等于常量 2。

如果你有:

void M(int x) { switch(x = 1) { case 2: ; } }

void M(int x) { x = 1; switch(x) { case 2: ; } }

然后你知道,我知道终点不可到达,但编译器不知道。规范中的规则是,可达性只能通过分析常量表达式来确定。任何包含变量的表达式,即使您通过其他方式知道它的值,也不是常量表达式。

过去,C# 编译器存在错误,但情况并非如此。你可以这样说:

void M(int x) { switch(x * 0) { case 2: ; } }

并且编译器会推断 x * 0 必须为 0,因此无法访问 case 标签。那是一个错误,我在 C# 3.0 中修复了它。规范说只有常量用于该分析,x 是一个变量,而不是一个常量。

现在,如果程序是合法的,那么编译器可以使用像这样的高级技术来影响生成的代码。如果你这样说:

void M(int x) { if (x * 0 == 0) Y(); }

然后编译器可以像你写的那样生成代码

void M(int x) { Y(); }

如果它愿意。但它不能使用x * 0 == 0 为真这一事实来确定语句的可达性。

最后,如果你有

void M(int x) { if (false) switch(x) { case 2: ; } }

然后我们知道开关不可到达,因此该块没有可到达的端点,所以这令人惊讶的是合法的。但是根据上面的讨论,你现在知道了

void M(int x) { if (x * 0 != 0) switch(x) { case 2: ; } }

不将x * 0 != 0 视为false,因此认为端点可达。

【讨论】:

  • 谢谢。我测试了switch(x) { },它可以编译,不是吗?
  • @KenKin:这是合法的;允许开关包含零开关部分。
【解决方案2】:

在 Visual Studio 2012 中,第一个原因是显而易见的。编译器判断代码不可达:

switch (1)
{
    case 2:
}

警告:检测到无法访问的代码。

在其他两种情况下,编译器报告“控制不能从一个案例标签 ('case 2:') 落到另一个”。在任何一个失败的案例中,我都没有看到它说“('case 1')”。

我猜编译器对持续评估并不积极。例如,以下是等价的:

int i;
switch(i=1)
{
    case 2:
}

int i = 1;
switch(i)
{
    case 2:
}

在这两种情况下,编译器都会尝试生成代码,当它可以进行评估并确定您正在编写的内容是:

switch (1)
{
    case 2:
}

并判断代码不可达。

我怀疑“为什么不编译”的答案将是“因为我们让 JIT 编译器处理激进的优化。”

【讨论】:

    【解决方案3】:

    好的,所以问题在于编译器完全优化掉了开关,这里是证明:

    static void withoutVar()
    {
        Console.WriteLine("Before!");
    
        switch (1)
        {
            case 2:
        }
    
        Console.WriteLine("After!");
    }
    

    当用 ILSpy 反编译时,它向我们展示了这个 IL:

    .method private hidebysig static 
        void withoutVar () cil managed 
    {
        // Method begins at RVA 0x2053
        // Code size 26 (0x1a)
        .maxstack 8
    
        IL_0000: nop
        IL_0001: ldstr "Before!"
        IL_0006: call void [mscorlib]System.Console::WriteLine(string)
        IL_000b: nop
        IL_000c: br.s IL_000e
    
        IL_000e: ldstr "After!"
        IL_0013: call void [mscorlib]System.Console::WriteLine(string)
        IL_0018: nop
        IL_0019: ret
    } // end of method Program::withoutVar
    

    在任何地方都不记得有 switch 语句。我认为它没有优化第二个的原因可能与运算符重载和排序有关。因此,我可能有一个自定义类型,当分配给1 时,它会变成2。但是,我不完全确定,在我看来应该提交错误报告。

    【讨论】:

    • "...优化了循环,"...什么循环?我想你的意思是切换。
    • 这不是错误;请不要提交报告。
    • @RichardJ.RossIII:谢谢。我不确定您的 bug 是什么意思;关于问题或关于开关表达式中的自定义类型。我使用自定义类型进行测试,该类型在int 之间实现了隐式运算符,并在分配1 时有意返回另一个值,这似乎不是问题。也许您在说其他错误?
    猜你喜欢
    • 1970-01-01
    • 2012-12-14
    • 2013-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-22
    相关资源
    最近更新 更多