我将逐步执行两个不同的代码块,以了解switch
和if
语句在执行方面的区别。
虽然if
似乎会检查每个条件,但switch
会直接跳到求值为true的条件。编译器如何知道哪个语句将在预检查时求值为true?请参见以下代码:
public void IFvSwitch()
{
string x = "f";
switch (x) // Compiler skips directly to case "f":
{
case "a":
MessageBox.Show(x);
break;
case "b":
MessageBox.Show(x);
break;
case "f":
MessageBox.Show(x);
break;
}
if (x == "a") // Compiler checks all conditions
{
MessageBox.Show(x);
}
else if (x == "b")
{
MessageBox.Show(x);
}
else if (x == "f")
{
MessageBox.Show(x);
}
}
5条答案
按热度按时间kkih6yb81#
免责声明
正如其他人所评论的那样,底层编译的IL与您看到的断点移动方式不同。在其内部,它实际上是逐个检查每个值。
但是,您的断点没有显示这一点。
阅读你的问题,我认为这里最重要的事情是你假设你的
if else if else if
结构是一个块;我的回答是从逻辑的Angular 来关注这种不正确的期望,而不是深入研究IL。你漏掉了一条重要线索:
这些都是单独的
if
语句,它们一个接一个地堆叠(在彼此的else
情况下)。它们单独执行是因为它们是单独的步骤!您的示例具有非常相似的
if
求值,但是您可以在这里做非常不同的事情:一次评估对下一次评估毫无意义,它们没有任何联系。
注意缩进(省略大括号)可以帮助展示这一点,缩进隐藏了这些步骤是嵌套的事实。
这个修改后的缩进,虽然冗长得不必要(尽管有些人喜欢这样),但在解释为什么这些是单独的步骤方面要清楚得多。
注意你没有问
if
如何知道如何进入第一种或第二种情况,这是相当明显的。一个
switch
实际上是一个具有两个以上结果的if
。你可以这样想:
switch
只需执行一个步骤:但是您的
if
福尔斯3种不同的if情况:您说得对,它们在语法上看起来非常相似,但是当您观察它们的逻辑流时,它们是非常不同的。
wqnecbli2#
在linqpad 4中,我编译了以下方法来查看底层IL:
生成的IL如下所示:
一般来说,您感兴趣的位从
IL_000C
开始,它加载开关变量,然后加载静态值“a”(IL_000D),比较它们(IL_0012),如果为真,则跳转至IL_0035(IL_0017)。然后,它对每个case语句重复此操作。在最后一个case语句之后,它跳到末尾(跳过每种情况下的所有代码)(IL_0033)。因此,您观察到的“'Switch'似乎直接跳到计算结果为true的条件”实际上并不正确。在单步调试调试时,它可能看起来像这样,但这并不代表底层编译代码的工作方式。
我应该注意到,现在switch语句总是这样编译的,只是这个特定的语句是这样编译的。
如果你增加case语句的数量,那么它将使用一个跳转表来实现它,在这种情况下,它创建一个私有字典,把你的case字符串作为键,然后用一个索引作为值,然后在字典中查找switch变量,并在跳转表中使用它来计算出在IL中将执行移动到哪里,所以在这种情况下,它 * 将 *能够以同样的方式直接转到右边的分支,当在字典中查找值时,它不需要检查每一个名称/值对。
Is there any significant difference between using if/else and switch-case in C#?(感谢@Sudsy1002提供链接)更详细地讨论了其中的一些内容。
goqiplq23#
TL;DR:* 逻辑上 *,switch语句中的每个case都被依次检查,第一个匹配的“wins”。作为一个 * 实现细节 *,编译器通常能够优化这一点。
当每个“case”都是常量时,编译器就可以生成一个跳转表来优化它,这在逻辑上总是和依次检查每个case给予相同的结果,它如何创建跳转表取决于开关类型,例如,切换整数比切换字符串简单。
当案例包含C#7中引入的模式时,编译器可能无法创建跳转表。下面的例子证明了可以在单个switch语句中检查多个
case
标签:输出为:
由于模式的开头不匹配,因此未记录“Second when”消息:我们还没有深入到保护子句(
when
),但是执行 * 可能 * 仍然需要检查它,实现 * 可能 * 会有效地切换类型,并且知道如果obj
是int
,它 * 只 * 需要检查第一个和第三个模式。基本上,从C# 7开始,编译器有很大的优化余地--当所有的case标签都是常量时,编译器非常非常有可能这样做--但 * 逻辑上 *,它需要一次检查一个case。
ewm0tg9j4#
我记得有人说过,一个带有
string
case的switch
内部会被转换成一个if-else结构,基本上就是说这只是语法上的糖衣。带int/enums的
switch
语句实际上的工作方式可能有所不同(因此比if/else更快)t8e9dugd5#
if似乎检查了每一个条件,而switch似乎直接跳到了求值为true的条件。编译器如何在预检查之前知道哪个语句求值为true?
让我们从纠正你的行话开始。C#编译器将C#翻译成IL。运行时有一个jit编译器将IL翻译成机器码。C#编译器还生成信息,通知 * 调试器 * 当你单步执行时它应该在哪里引起中断。
因此,我们并不清楚您的问题是什么。是“C#编译器如何生成IL以避免线性搜索?”还是“C#编译器如何通知调试器它应该跳过显示开关代码生成器的搜索部分?”还是“JIT编译器如何为开关生成机器码?”
告诉你吧,我会在这里挥挥手,给予你一些模糊的答案,描述 * 策略 *,但不描述细节。如果你对细节有疑问,那么就提出一个更具体的问题。特别是,我会使用“编译器”来表示C#编译器或JIT编译器,但不会说明是哪一个。有时工作是由C#编译器完成的,有时它会把工作交给抖动。但不管怎样,任务都完成了
Switch代码生成很复杂。让我们从简单的开始。编译器可以选择编译一个switch,就好像它是一堆if-else语句一样,实际上它经常这样做。特别是当switch很小的时候,就像你的例子一样。
调试器如何知道不向您显示实际发生的比较步骤?编译器还生成调试信息,通知调试器在单步执行时哪些指令应该跳过,哪些指令应该生成自动断点。因此,C#编译器、JIT编译器和调试器一起工作,以确保开发人员在单步执行开关时有正确的体验。
如果switch很大会发生什么?执行大量的if-else来找到正确的switch部分可能会很慢。让我们再做一个简单的例子,看看一个充满字符串的大switch。在这种情况下,编译器会做以下事情:
现在,快速查找字符串而不检查每一个字符串的工作留给了字典,字典使用哈希表将字符串比较的次数减少了一个巨大的因子。
如果我们有一个很大的开关,但是大小写都是整数呢?
在这种情况下,编译器可以创建一个包含一千个开关情况地址的静态数组,然后生成如下代码:
现在我们只剩下两次比较和一次数组解引用;这确实直接跳到了正确的部分。
这个呢?
这里没有case 2000,在这种情况下编译器可以像以前一样生成一个跳转表,有2002个条目,在2000槽中,它只放置默认部分的地址。
这个呢?
现在我们遇到了一个有趣的情况!编译器不会生成一个包含1000002个条目的跳转表,其中大部分都是“转到默认情况”。在这种情况下,编译器可以生成一个混合开关:
等等,我相信你们可以看到,当开关很大,并且有很多密集的取值范围时,这会变得非常复杂。
优化社区已经做了大量工作,研究创建混合交换机策略的最佳平衡。
也有很多出错的机会。我将用一个90年代的战争故事来结束。那时的MSVC编译器--我想是版本4 --有一个bug,在我的特定程序中,混合跳转表生成不正确。基本上,它在不需要的时候,把一堆连续的范围分解成单独的跳转表。
正常情况下,这不是问题,但这个特定的开关是Microsoft VBScript和JScript引擎中最内层的逻辑循环,它调度脚本中的下一条指令。(VBScript和JScript当时编译为专有字节码语言。)这个错误的开关会降低脚本引擎的性能。
MSVC团队无法为我们快速地修复bug,所以我们最终编写了英雄宏代码,让我们以一种看起来合理的方式来表达switch语句,但在幕后宏会生成适当的跳转表!
幸运的是,你可以依靠C#编译器和jitter为你生成好的代码,你不必担心为开关生成什么代码。