C#switch语句限制-为什么?

时间:2020-03-05 18:47:56  来源:igfitidea点击:

编写switch语句时,在case语句中可以打开的内容似乎有两个限制。

例如(是的,我知道,如果我们正在做这种事情,那可能意味着面向对象(OO)架构是虚假的,这只是一个虚构的示例!),

Type t = typeof(int);

  switch (t) {

    case typeof(int):
      Console.WriteLine("int!");
      break;

    case typeof(string):
      Console.WriteLine("string!");
      break;

    default:
      Console.WriteLine("unknown!");
      break;
  }

在这里,switch()语句以"期望整数类型的值"失败,而case语句以"期望常数的值"失败。

为什么要设置这些限制?其基本依据是什么?我看不出为什么switch语句仅必须屈服于静态分析,以及为什么打开的值必须是整数(即原始)的任何原因。理由是什么?

解决方案

回答

这是我的原始帖子,引发了一些争论...因为它是错误的:

The switch statement is not the same
  thing as a big if-else statement. 
  Each case must be unique and evaluated
  statically.  The switch statement does
  a constant time branch regardless of
  how many cases you have.  The if-else
  statement evaluates each condition
  until it finds one that is true.

实际上,Cswitch语句并不总是恒定时间分支。

在某些情况下,编译器将使用CIL switch语句,该语句实际上是使用跳转表的恒定时间分支。但是,在稀疏情况下,如Ivan Hamilton指出的,编译器可能会完全生成其他内容。

通过编写各种Cswitch语句(有些稀疏,有些密集)并使用ildasm.exe工具查看生成的CIL,实际上很容易验证。

回答

这不是原因,但是Cspecification部分8.7.2声明以下内容:

The governing type of a switch statement is established by the switch expression. If the type of the switch expression is sbyte, byte, short, ushort, int, uint, long, ulong, char, string, or an enum-type, then that is the governing type of the switch statement. Otherwise, exactly one user-defined implicit conversion (§6.4) must exist from the type of the switch expression to one of the following possible governing types: sbyte, byte, short, ushort, int, uint, long, ulong, char, string. If no such implicit conversion exists, or if more than one such implicit conversion exists, a compile-time error occurs.

C3.0规范位于:
http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc

回答

我想编译器无法将switch语句自动转换为以下内容没有根本原因:

if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...

但这并没有太大的收获。

关于整数类型的case语句使编译器可以进行许多优化:

  • 没有重复(除非我们重复了大小写标签,编译器会检测到)。在示例中,由于继承,t可以匹配多种类型。应该执行第一个比赛吗?他们都是?
  • 编译器可以选择通过跳转表在整数类型上实现switch语句,以避免所有比较。如果我们要打开一个具有0到100的整数值的枚举,那么它将创建一个包含100个指针的数组,每个switch语句一个。在运行时,它仅基于打开的整数值从数组中查找地址。与执行100次比较相比,这可以使运行时性能更好。

回答

根据switch语句文档,如果存在将对象隐式转换为整数类型的明确方法,则将允许该方法。我认为我们期望这样一种行为:对于每个case语句,它都将被if(t == typeof(int))取代,但是当我们使该运算符重载时,它将打开一堆蠕虫。如果我们错误地编写了==覆盖,则更改switch语句的实现详细信息时,该行为也会更改。通过减少对整数类型和字符串的比较以及可以简化为整数类型(并且打算这样做)的那些事物,它们避免了潜在的问题。

回答

杰夫·阿特伍德(Jeff Atwood)谈到这个话题时,switch语句是编程上的残暴行为。谨慎使用它们。

我们通常可以使用表来完成相同的任务。例如:

var table = new Dictionary<Type, string>()
{
   { typeof(int), "it's an int!" }
   { typeof(string), "it's a string!" }
};

Type someType = typeof(int);
Console.WriteLine(table[someType]);

回答

我几乎不了解C#,但是我怀疑这两种切换只是在其他语言中发生时才采取的,而没有考虑使其更通用,或者开发人员认为扩展它是不值得的。

严格来说,我们是绝对正确的,没有理由对此施加任何限制。有人可能会怀疑,原因是对于允许的情况,实现非常有效(如Brian Ensink(44921)所建议),但是我怀疑如果我使用整数和某些随机情况,实现是否非常有效(wrt if语句)。 (例如345,-4574和1234203)。在任何情况下,将其用于所有内容(或者至少更多内容)并说仅对特定情况(例如(几乎)连续数)有效,这有什么害处?

但是,我可以想象,由于诸如lomaxx(44918)给出的原因,可能要排除类型。

编辑:@Henk(44970):如果最大程度地共享字符串,则内容相同的字符串也将指向相同的内存位置。然后,如果我们可以确保将用例中使用的字符串连续存储在内存中,则可以非常有效地实现切换(即执行顺序为2比较,加法和两次跳转)。

回答

wrote:
  
  
    "The switch statement does a constant time branch regardless of how many cases you have."

由于该语言允许在switch语句中使用字符串类型,因此我假设编译器无法为该类型的恒定时间分支实现生成代码,并且需要生成if-then样式。

@mweerden啊,我明白了。谢谢。

我在Cand .NET方面没有很多经验,但是似乎语言设计者不允许在狭窄情况下静态访问类型系统。 typeof关键字返回一个对象,因此只能在运行时访问。

回答

我认为Henk用"绝对禁止访问类型系统"来钉牢它

另一个选择是没有顺序键入数字和字符串。因此,类型开关将无法构建二进制搜索树,而只能构建线性搜索。

回答

我同意这种意见,即使用表驱动方法通常更好。

在C1.0中,这是不可能的,因为它没有泛型和匿名委托。
Chave的新版本脚手架可以完成这项工作。使用对象文字表示法也有帮助。

回答

通常,由于语言设计师的缘故,这些限制已经到位。基本的理由可能是与languange历史的兼容性,理想情况或者简化编译器设计。

编译器可以(并且确实)选择:

  • 创建一个大的if-else语句
  • 使用MSIL切换指令(跳转表)
  • 生成Generic.Dictionary <string,int32>,在首次使用时进行填充,然后调用Generic.Dictionary <> :: TryGetValue()获取索引以传递给MSIL切换指令(跳转表)
  • 使用if-elses和MSIL" switch"跳转的组合

switch语句不是恒定时间分支。编译器可能会找到捷径(使用哈希桶等),但是更复杂的情况将生成更复杂的MSIL代码,其中某些情况比其他情况更早分支出来。

为了处理String的情况,编译器将最终使用a.Equals(b)(可能还使用a.GetHashCode())(在某些时候)。我认为使用满足这些约束的任何对象对于编译器来说都是很简单的。

至于对静态case表达式的需求...如果case表达式不是确定性的,那么其中的一些优化(哈希,缓存等)将不可用。但是我们已经看到,有时编译器还是会选择简单的if-else-if-else之路...

编辑:lomaxx我们对" typeof"运算符的理解不正确。 " typeof"运算符用于获取类型的System.Type对象(与它的超类型或者接口无关)。检查具有给定类型的对象的运行时兼容性是" is"操作员的工作。在此使用" typeof"表示对象是不相关的。

回答

顺便说一句,具有相同基础架构的VB允许使用更灵活的" Select Case"语句(上述代码将在VB中运行),并且在可能的情况下仍会生成有效的代码,因此必须谨慎考虑基于技术约束的参数。

回答

我想到的第一个原因是历史原因:

由于大多数C,C ++和Java程序员都不习惯拥有这种自由,因此他们并不需要这些自由。

另一个更有效的原因是语言的复杂性将会增加:

首先,应该将对象与.Equals()还是与==运算符进行比较?两者在某些情况下均有效。我们应该引入新的语法来做到这一点吗?我们应该允许程序员介绍他们自己的比较方法吗?

另外,允许打开对象会破坏有关switch语句的基本假设。有两个规则控制switch语句,如果允许打开对象,则编译器将无法执行该语句(请参阅Cversion 3.0语言规范,8.7.2):

  • 开关标签的值是恒定的
  • 开关标签的值是不同的(因此,对于给定的开关表达式,只能选择一个开关块)

在假设情况下允许使用非恒定大小写值的情况下,请考虑以下代码示例:

void DoIt()
{
    String foo = "bar";
    Switch(foo, foo);
}

void Switch(String val1, String val2)
{
    switch ("bar")
    {
        // The compiler will not know that val1 and val2 are not distinct
        case val1:
            // Is this case block selected?
            break;
        case val2:
            // Or this one?
            break;
        case "bar":
            // Or perhaps this one?
            break;
    }
}

该代码将做什么?如果案例陈述被重新排序怎么办?确实,Cmade switch掉线非法的原因之一是可以任意重新排列switch语句。

这些规则之所以到位,是因为程序员可以通过查看一个案例块来确定输入该块的确切条件。当前面提到的switch语句增长到100行或者更多(并且将会)时,这样的知识是非常宝贵的。

回答

重要的是不要将Cswitch语句与CIL switch指令混淆。

CIL开关是一个跳转表,它需要一个指向一组跳转地址的索引。

这仅在Cswitch的案例相邻时才有用:

case 3: blah; break;
case 4: blah; break;
case 5: blah; break;

但是如果没有的话就没什么用了:

case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;

(我们需要一个约3000个条目的表,仅使用3个插槽)

使用不相邻的表达式,编译器可能会开始执行线性if-else-if-else检查。

对于较大的不相邻的表达式集,编译器可以从二叉树搜索开始,最后是if-else-if-else最后几项。

使用包含大量相邻项的表达式集,编译器可以进行二叉树搜索,最后是CIL开关。

它充满了" mays"和" mights",并且取决于编译器(可能与Mono或者Rotor不同)。

我使用相邻的案例在计算机上复制了结果:

total time to execute a 10 way switch, 10000 iterations (ms) : 25.1383

  approximate time per 10 way switch (ms)                      : 0.00251383
  
  total time to execute a 50 way switch, 10000 iterations (ms) : 26.593

  approximate time per 50 way switch (ms)                      : 0.0026593
  
  total time to execute a 5000 way switch, 10000 iterations (ms) : 23.7094

  approximate time per 5000 way switch (ms)                      : 0.00237094
  
  total time to execute a 50000 way switch, 10000 iterations (ms) : 20.0933

  approximate time per 50000 way switch (ms)                      : 0.00200933

然后,我也使用了非相邻的case表达式:

total time to execute a 10 way switch, 10000 iterations (ms) : 19.6189

  approximate time per 10 way switch (ms)                      : 0.00196189
  
  total time to execute a 500 way switch, 10000 iterations (ms) : 19.1664

  approximate time per 500 way switch (ms)                      : 0.00191664
  
  total time to execute a 5000 way switch, 10000 iterations (ms) : 19.5871

  approximate time per 5000 way switch (ms)                      : 0.00195871
  
  A non-adjacent 50,000 case switch statement would not compile.

  "An expression is too long or complex to compile near 'ConsoleApplication1.Program.Main(string[])'

有趣的是,二叉树搜索的速度比CIL切换指令的显示速度快一些(可能不是统计上的)。

Brian,我们使用过"常数"一词,从计算复杂性理论的角度来看,它具有非常明确的含义。简单的相邻整数示例可能会产生被认为是O(1)(常数)的CIL,而稀疏示例是O(log n)(对数),聚类示例位于两者之间,而小示例是O(n)(线性)。

这甚至不能解决String的情况,在这种情况下,可能会创建静态Generic.Dictionary &lt;string,int32>,并且在首次使用时会遭受一定的开销。这里的性能将取决于" Generic.Dictionary"的性能。

如果检查"语言规范"(不是CIL规范)
我们会发现" 15.7.2 switch语句"没有提到"恒定时间",或者底层实现甚至使用了CIL switch指令(要非常小心地假设这些事情)。

归根结底,在现代系统上针对整数表达式的Cswitch是亚微秒级的操作,通常不值得担心。

当然,这些时间取决于机器和条件。我不会注意这些时序测试,正在谈论的微秒持续时间与正在运行的任何实际代码(我们必须包括一些实际代码,否则编译器会优化分支)相形见or,或者使系统抖动。我的答案基于使用IL DASM来检查由Ccompiler创建的CIL。当然,这不是最终的,因为JIT随后会创建CPU运行的实际指令。

我已经检查了x86机器上实际执行的最终CPU指令,并可以确认一个简单的相邻set开关执行以下操作:

jmp     ds:300025F0[eax*4]

在二叉树搜索中满是:

cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  …
  cmp     ebx, 0F82h
  jz      30005EEE

回答

I don't see any reason why the switch statement has to succomb to static analysis only

没错,这不是必须的,实际上许多语言都使用动态switch语句。但是,这意味着对" case"子句进行重新排序可以更改代码的行为。

在此处的" switch"设计决策背后有一些有趣的信息:为什么Cswitch语句被设计为不允许失败,但仍需要中断?

允许动态的大小写表达式会导致诸如以下PHP代码之类的怪异现象:

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

坦率地说,应该只使用if-else语句。

回答

犹大的上述回答给了我一个主意。我们可以使用Dictionary &lt;Type,Func &lt;T>来"伪造" OP的切换行为:

Dictionary<Type, Func<object, string,  string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
                    {
                        return string.Format("{0}: {1}", s, o.ToString());
                    });

这使我们可以将行为与具有与switch语句相同样式的类型相关联。我相信,当编译为IL时,它具有被键控而不是开关式跳转表的额外好处。