代码之家  ›  专栏  ›  技术社区  ›  ljs TheVillageIdiot

C#开关语句限制-为什么?

  •  131
  • ljs TheVillageIdiot  · 技术社区  · 17 年前

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

    object-oriented (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语句有什么原因 仅服从静态分析,以及为什么打开的值必须是整数(即基本值)。理由是什么?

    16 回复  |  直到 11 年前
        1
  •  115
  •   Ivan Hamilton    11 年前

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

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

    这仅在C#开关的外壳相邻时有用:

    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最后几项。

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

    这充满了“可能”&“可能”,它取决于编译器(可能与Mono或Rotor不同)。

    执行10路切换的总时间,10000次迭代(毫秒):25.1383
    每10路开关的近似时间(ms):0.00251383

    执行50路切换的总时间,10000次迭代(ms):26.593
    每50路开关的近似时间(ms):0.0026593

    执行5000路开关的总时间,10000次迭代(毫秒):23.7094
    每5000路开关的近似时间(ms):0.00237094


    每50000路开关的近似时间(ms):0.00200933

    然后我还使用了非相邻大小写表达式:

    执行10路切换的总时间,10000次迭代(毫秒):19.6189

    执行500路切换的总时间,10000次迭代(毫秒):19.1664
    每500路开关的近似时间(ms):0.00191664

    执行5000路开关的总时间,10000次迭代(毫秒):19.5871
    每5000路开关的近似时间(ms):0.00195871

    无法编译非相邻的50000 case switch语句。

    有趣的是,二叉树搜索似乎比CIL开关指令快一点(可能不是统计上的)。

    布莱恩,你用过这个词“ “,从计算复杂性理论的角度来看,它有一个非常明确的含义。虽然简化的相邻整数示例可能产生被认为是O(1)(常数)的CIL,但稀疏示例是O(对数n)(对数),聚集示例介于两者之间,小示例是O(n)(线性)。

    这甚至不能解决字符串的情况,在这种情况下 Generic.Dictionary<string,int32> 可能会被创建,并且在首次使用时将承受一定的开销。此处的性能将取决于 Generic.Dictionary

    如果你查一下 C# Language Specification (不是CIL规范)

    归根结底,在现代系统中,对整数表达式进行C#切换是一种亚微秒级的操作,通常不值得担心。


    当然,这些时间将取决于机器和条件。我不会注意这些计时测试,我们所说的微秒持续时间与正在运行的任何实际代码(必须包含一些实际代码,否则编译器将优化分支)或系统中的抖动相比都是微不足道的。我的答案是基于使用 IL DASM 检查由C#编译器创建的CIL。当然,这不是最终的,因为CPU运行的实际指令随后由JIT创建。

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

      jmp     ds:300025F0[eax*4]
    

      cmp     ebx, 79Eh
      jg      3000352B
      cmp     ebx, 654h
      jg      300032BB
      …
      cmp     ebx, 0F82h
      jz      30005EEE
    
        2
  •  103
  •   Community Mohan Dere    8 年前

    这是我最初的帖子,引发了一些争论。。。 因为这是错误的 :

    作为一个大的if-else语句。 每个案例都必须是唯一的并经过评估 恒定时间分支,不考虑 你有多少箱。如果还有 语句计算每个条件 直到它找到一个真实的。


    在某些情况下,编译器将使用CIL switch语句,该语句实际上是使用跳转表的常量时间分支。然而,正如 Ivan Hamilton 编译器可能会完全生成其他内容。

        3
  •  23
  •   Antti Kissaniemi    17 年前

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

    另一个更合理的原因是 语言复杂性会增加 :

    首先,应该将对象与对象进行比较吗 .Equals() 或者用 ==

    此外,允许打开对象将 打破关于switch语句的基本假设 . 如果允许打开对象,编译器将无法强制执行两条规则来管理switch语句(请参阅 C# version 3.0 language specification , §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;
        }
    }
    

    代码将做什么?如果case语句被重新排序呢?事实上,C#使switch失败为非法的原因之一是switch语句可以任意重新排列。

    这些规则的存在是有原因的——因此程序员可以通过查看一个case块,确定输入块的确切条件。当前面提到的switch语句增长到100行或更多行时(它将增长),这样的知识是非常宝贵的。

        4
  •  10
  •   Konrad Rudolph    17 年前

    Select Case 语句(上面的代码将在VB中工作),并且在可能的情况下仍然生成高效的代码,因此必须仔细考虑技术约束的参数。

        5
  •  10
  •   Community Mohan Dere    8 年前

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

    • 创建一个大的if-else语句
    • 使用MSIL开关指令(跳转表)
    • 通用词典<>::TryGetValue() 指令(跳转表)
    • 使用 “开关”跳跃

    switch语句不是常量时间分支。编译器可能会找到捷径(使用散列桶等),但更复杂的情况会生成更复杂的MSIL代码,有些情况会比其他情况更早分支。

    为了处理字符串大小写,编译器最终将使用a.Equals(b)(可能还有a.GetHashCode())。我认为编译器使用任何满足这些约束的对象都是很繁琐的。

    至于对静态大小写表达式的需要。。。如果case表达式不是确定性的,那么其中一些优化(散列、缓存等)将不可用。但是我们已经看到,有时候编译器只是选择了简单的if-else-if-else道路。。。

    编辑: lomaxx -您对“typeof”运算符的理解不正确。“typeof”操作符用于获取类型的System.Type对象(与其超类型或接口无关)。检查对象与给定类型的运行时兼容性是“is”操作符的工作。这里使用“typeof”来表示对象是不相关的。

        6
  •  7
  •   Bernard Vander Beken Harald Coppoolse    8 年前

    现在使用C#7,您可以:

    switch(shape)
    {
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
    }
    
        7
  •  6
  •   Judah Gabriel Himango    17 年前

    根据杰夫·阿特伍德的说法, the switch statement is a programming atrocity . 少用。

    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]);
    
        8
  •  6
  •   Roman Starkov    16 年前

    我看不出为什么switch语句只能进行静态分析

    没错,没有 事实上,许多语言都使用动态开关语句。然而,这意味着重新排列“case”子句可以改变代码的行为。

    Why is the C# switch statement designed to not allow fall-through, but still require a break?

    允许动态大小写表达式可能导致以下PHP代码:

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

    坦白地说,应该使用 if-else

        9
  •  3
  •   markus    17 年前

    switch语句的控制类型由switch表达式建立。如果switch表达式的类型是sbyte、byte、short、ushort、int、uint、long、ulong、char、string或枚举类型,则这是switch语句的控制类型。否则,从开关表达式的类型到以下可能的控制类型之一,必须存在一个用户定义的隐式转换(§6.4):sbyte、byte、short、ushort、int、uint、long、ulong、char、string。如果不存在此类隐式转换,或者存在多个此类隐式转换,则会发生编译时错误。

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

        10
  •  3
  •   Dave Swersky    15 年前

    上面犹大的回答给了我一个想法。您可以在上面使用 Dictionary<Type, Func<T> :

    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时,它是键控的,而不是开关式的跳转表。

        11
  •  0
  •   Rob Walker    17 年前

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

    但这并没有什么好处。

    整数类型的case语句允许编译器进行大量优化:

    1. 编译器可以选择通过跳转表在整数类型上实现switch语句,以避免所有比较。如果要打开整数值为0到100的枚举,则它将创建一个包含100个指针的数组,每个switch语句一个指针。在运行时,它只是根据打开的整数值从数组中查找地址。这使得运行时性能比执行100次比较要好得多。

        12
  •  0
  •   fryguybob    17 年前

    根据 the switch statement documentation if (t == typeof(int)) ,但当你让操作员超负荷工作时,这会打开一整罐蠕虫。如果不正确地编写==重写,则当switch语句的实现详细信息更改时,该行为将更改。通过减少与整型和字符串的比较,以及那些可以简化为整型(并打算)的东西,它们避免了潜在的问题。

        13
  •  0
  •   Henk    17 年前

    “无论您有多少个案例,switch语句都会执行恒定时间分支。”

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

    我在C#和.NET方面没有太多的经验,但似乎语言设计者不允许静态访问类型系统,除非在狭窄的环境中。这个 类型

        14
  •  0
  •   BCS    17 年前

    另一个选择是,对于可以使用数字和字符串的类型,没有顺序。因此,类型开关不能构建二进制搜索树,只能构建线性搜索。

        15
  •  0
  •   Community Mohan Dere    8 年前

    我同意 this comment 使用表驱动的方法通常更好。

    在C#1.0中,这是不可能的,因为它没有泛型和匿名委托。

        16
  •  0
  •   Community Mohan Dere    8 年前

    严格地说,你完全正确,没有理由对它施加这些限制。有人可能会怀疑,原因是对于允许的情况,实现是非常有效的(正如Brian Ensink所建议的)( 44921

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

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

        17
  •  0
  •   smolchanovsky    5 年前

    public string GetTypeName(object obj)
    {
        return obj switch
        {
            int i => "Int32",
            string s => "String",
            { } => "Unknown",
            _ => throw new ArgumentNullException(nameof(obj))
        };
    }
    

    因此,您会得到:

    Console.WriteLine(GetTypeName(obj: 1));           // Int32
    Console.WriteLine(GetTypeName(obj: "string"));    // String
    Console.WriteLine(GetTypeName(obj: 1.2));         // Unknown
    Console.WriteLine(GetTypeName(obj: null));        // System.ArgumentNullException
    

    here .