【问题标题】:Why was the switch statement designed to need a break?为什么 switch 语句被设计为需要中断?
【发布时间】:2008-10-31 02:56:42
【问题描述】:

给定一个简单的 switch 语句

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

case 2 中没有 break 语句,意味着 case 3 的代码将继续执行。 这不是意外;它就是这样设计的。为什么做出这个决定?与块的自动中断语义相比,这有什么好处?理由是什么?

【问题讨论】:

    标签: c language-design


    【解决方案1】:

    许多答案似乎都集中在失败的能力上,作为需要break 声明的原因

    我认为这只是一个错误,主要是因为在设计 C 语言时,并没有太多关于如何使用这些结构的经验。

    Peter Van der Linden 在他的“专家 C 编程”一书中提出了这个案例:

    我们分析了 Sun C 编译器源 查看默认下降的频率 通过使用。太阳 ANSI C 编译器前端有 244 开关 语句,每个语句都有一个 平均七例。跌倒 仅发生在所有这些案例中的 3%。

    也就是说,正常的开关 行为是错误 97% 的时间。 它不仅仅是在编译器中 - 在 相反,在使用跌落的地方 在这个分析中,通常是因为 更频繁发生的情况 在编译器中比在其他软件中, 例如,在编译运算符时 可以有一个或两个 操作数:

    switch (operator->num_of_operands) {
        case 2: process_operand( operator->operand_2);
                  /* FALLTHRU */
    
        case 1: process_operand( operator->operand_1);
        break;
    }
    

    案例失败如此广泛 被认为是存在的缺陷 甚至是一个特别的评论约定, 如上所示,它告诉 lint “这是 真的是那 3% 的案例之一 想要跌倒。”

    我认为对于 C# 来说,在每个 case 块的末尾需要一个明确的跳转语句是个好主意(同时仍然允许堆叠多个 case 标签 - 只要只有一个语句块)。在 C# 中,您仍然可以让一个案例落入另一个案例 - 您只需通过使用 goto 跳转到下一个案例来明确跌落。

    Java 没有借此机会打破 C 语义,真是太糟糕了。

    【讨论】:

    • 确实,我认为他们是为了简化实施。其他一些语言支持更复杂的情况(范围、多个值、字符串...),但可能是以效率为代价的。
    • Java 可能不想打破习惯并传播混乱。对于不同的行为,他们将不得不使用不同的语义。无论如何,Java 设计人员失去了许多摆脱 C 的机会。
    • @PhiLho - 我认为您可能最接近“实施简单性”的真相。
    • 如果您通过 GOTO 使用显式跳转,那么使用一系列 IF 语句不是同样有效吗?
    • 甚至 Pascal 也能不间断地实现他们的 switch。 C 编译器编码器怎么可能不考虑它@@
    【解决方案2】:

    在很多方面,c 只是标准汇编习惯用法的干净接口。在编写跳转表驱动流控制时,程序员可以选择是跳出还是跳出“控制结构”,而跳出需要显式指令。

    所以,c 做同样的事情...

    【讨论】:

    • 我知道很多人都这么说,但我认为这不是全貌; C 也经常是汇编反模式的丑陋接口。 1. 丑陋:语句而不是表达式。 “声明反映使用。”将复制粘贴美化为模块化和可移植性的 机制。类型系统方式太复杂了; 鼓励错误。 NULL。 (在下一条评论中继续。)
    • 2.反模式:不提供“干净的”标记联合,这是汇编的两个基本数据表示习惯用法之一。 (Knuth,第 1 卷,第 2 章)取而代之的是“未标记的工会”,这是一种非惯用的 hack。几十年来,这种选择一直困扰着人们对数据的看法。 And NUL-terminated strings are just about the worst idea ever.
    • @HarrisonKlaperman:没有一种存储字符串的方法是完美的。如果接受字符串的例程也接受最大长度参数,则与空终止字符串相关的绝大多数问题都不会发生,并且会发生在将长度标记的字符串存储到固定大小的缓冲区而不接受缓冲区大小参数的例程中.在现代人看来,case 只是标签的 switch 语句的设计可能看起来很奇怪,但它并不比 Fortran DO 循环的设计差。
    • 如果我决定用汇编写一个跳转表,我会取case值,然后神奇地把它变成跳转表下标,跳转到那个位置,然后执行代码。在那之后,我不会跳入下一个案例。我将跳转到一个统一的地址,即所有案例的出口。我会跳入或跌入下一个案件的想法是愚蠢的。该模式没有用例。
    • 虽然现在人们更多地习惯于清洁和自我保护 idos 和防止自己射到脚上的语言特征,但这是从字节昂贵的领域的回忆(C 早在1970)。如果您的代码需要在 1024 字节以内,您将面临重用代码片段的沉重压力。从共享同一端的不同入口点开始重用代码是实现这一目标的一种机制。
    【解决方案3】:

    显然要实现达夫的设备:

    dsend(to, from, count)
    char *to, *from;
    int count;
    {
        int n = (count + 7) / 8;
        switch (count % 8) {
        case 0: do { *to = *from++;
        case 7:      *to = *from++;
        case 6:      *to = *from++;
        case 5:      *to = *from++;
        case 4:      *to = *from++;
        case 3:      *to = *from++;
        case 2:      *to = *from++;
        case 1:      *to = *from++;
                   } while (--n > 0);
        }
    }
    

    【讨论】:

    • 我喜欢达夫的设备。如此优雅,速度极快。
    • 是的,但是每次 SO 上有 switch 语句时,它都必须显示吗?
    • 你错过了两个右花括号;-)。
    【解决方案4】:

    如果案例被设计为隐式中断,那么您就不可能失败。

    case 0:
    case 1:
    case 2:
        // all do the same thing.
        break;
    case 3:
    case 4:
        // do something different.
        break;
    default:
        // something else entirely.
    

    如果开关被设计为在每个案例后隐式中断,您将别无选择。开关盒结构的设计方式更加灵活。

    【讨论】:

    • 您可以想象一个隐式中断但具有“fallthrough”关键字的开关。尴尬,但可行。
    • 这样会更好吗?我想一个案例语句比“每个案例的代码块”更频繁地以这种方式工作......这是一个 if/then/else 块。
    • 我要在问题中补充一点,每个 case 块中的范围附件 {} 都会增加混乱,因为它看起来像“while”语句的样式。
    • @Bill:我认为这会更糟,但它会解决 Mike B 提出的投诉:尽管跌倒(除了多个情况相同)是罕见的事件,不应该成为默认行为。
    • 为了简洁起见,我省略了大括号,它们应该有多个语句。我同意只有在情况完全相同时才应使用fallthrough。当案例使用 fallthrough 建立在以前的案例之上时,这会令人困惑。
    【解决方案5】:

    switch 语句中的 case 语句只是标签。

    当您打开一个值时,switch 语句本质上是对具有匹配值的标签执行 goto

    这意味着break是必要的,以避免传递到下一个标签下的代码。

    至于为什么以这种方式实现的原因 - switch 语句的贯穿特性在某些情况下可能很有用。例如:

    case optionA:
        // optionA needs to do its own thing, and also B's thing.
        // Fall-through to optionB afterwards.
        // Its behaviour is a superset of B's.
    case optionB:
        // optionB needs to do its own thing
        // Its behaviour is a subset of A's.
        break;
    case optionC:
        // optionC is quite independent so it does its own thing.
        break;
    

    【讨论】:

    • 有两个大问题:1)在需要的地方忘记了break。 2) 如果 case 语句的顺序发生了变化,则失败可能会导致运行错误的 case。因此,我发现 C# 的处理能力要好得多(显式 goto case 表示失败,空大小写标签除外)。
    • @DanielRose: 1) 在 C# 中也有一些方法可以忘记 break - 最简单的方法是当您不想让 case 做任何事情但忘记添加 break 时(也许你被解释性的评论所吸引,或者被其他任务叫走):执行将落到下面的case。 2) 鼓励goto case 鼓励非结构化的“意大利面条”编码 - 您可能会遇到意外循环 (case A: ... goto case B; case B: ... ; goto case A;),尤其是当案例在文件中分开并且难以组合时。在 C++ 中,失败是本地化的。
    【解决方案6】:

    允许这样的事情:

    switch(foo) {
    case 1:
        /* stuff for case 1 only */
        if (0) {
    case 2:
        /* stuff for case 2 only */
        }
        /* stuff for cases 1 and 2 */
    case 3:
        /* stuff for cases 1, 2, and 3 */
    }
    

    case 关键字视为goto 标签,它就自然多了。

    【讨论】:

    • 第一个案例末尾的if(0) 是邪恶的,只有在目标是混淆代码时才应该使用。
    • 我认为,整个答案都是在练习作恶。 :-)
    【解决方案7】:

    当多个案例需要执行相同的代码(或按顺序执行相同的代码)时,它消除了代码重复。

    由于在汇编语言级别上,它并不关心您是否在每一个之间中断,无论如何,失败案例的开销为零,所以为什么不允许它们,因为它们在某些情况下具有显着优势。

    【讨论】:

      【解决方案8】:

      我碰巧遇到了将向量中的值分配给结构的情况:必须以这样一种方式完成,即如果数据向量小于结构中数据成员的数量,则其余成员将保持其默认值。在那种情况下,省略 break 非常有用。

      switch (nShorts)
      {
      case 4: frame.leadV1    = shortArray[3];
      case 3: frame.leadIII   = shortArray[2];
      case 2: frame.leadII    = shortArray[1];
      case 1: frame.leadI     = shortArray[0]; break;
      default: TS_ASSERT(false);
      }
      

      【讨论】:

        【解决方案9】:

        正如这里许多人所指定的,它允许单个代码块适用于多种情况。这应该在您的 switch 语句中比您在示例中指定的“每个案例的代码块”更常见。

        如果您每个案例都有一个代码块而没有失败,也许您应该考虑使用 if-elseif-else 块,因为这似乎更合适。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-12-26
          • 1970-01-01
          • 1970-01-01
          • 2019-10-27
          • 1970-01-01
          • 2010-09-30
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多