switch语句应始终包含默认子句吗?


254

在我的第一批代码审阅中(有一段时间),我被告知,在所有switch语句中包括default子句是一种很好的做法。我最近记得这个建议,但不记得有什么道理。现在对我来说听起来很奇怪。

  1. 是否有一个总是包含默认语句的合理原因?

  2. 这种语言依赖吗?我不记得当时使用的是哪种语言-也许这适用于某些语言,而不适用于其他语言?


9
很大程度上取决于语言
skaffman 2011年

Answers:


276

开关盒几乎总是应该有一个default盒。

使用理由 default

1.“捕捉”意外值

switch(type)
{
    case 1:
        //something
    case 2:
        //something else
    default:
        // unknown type! based on the language,
        // there should probably be some error-handling
        // here, maybe an exception
}

2.处理“默认”操作,这种情况属于特殊行为。

您会在菜单驱动程序和bash shell脚本中看到很多。当变量在switch-case之外声明但未初始化时,您可能还会看到此情况,并且每种情况都会将其初始化为不同的东西。在这里,默认值也需要对其进行初始化,以便访问该变量的代码行不会引发错误。

3.让某人阅读您的代码,您已经解决了这种情况。

variable = (variable == "value") ? 1 : 2;
switch(variable)
{
    case 1:
        // something
    case 2:
        // something else
    default:
        // will NOT execute because of the line preceding the switch.
}

这是一个过于简化的示例,但重点是,阅读代码的人不要奇怪为什么variable不能是1或2以外的东西。


我能想到的唯一不使用的情况default是,当开关正在检查某项东西时,可以很容易地忽略其他所有明显的选择

switch(keystroke)
{
    case 'w':
        // move up
    case 'a':
        // move left
    case 's':
        // move down
    case 'd':
        // move right
    // no default really required here
}

25
您关于击键的示例与您刚才所说的其余部分相矛盾。:| 我认为应该有更多的解释,例如:在这种情况下,您不必关心默认值,因为如果您按下另一个键,则您不在乎,但是如果传入的变量与预期的不同,我们会注意。
安德鲁

1
同时,如果您正在检查GET参数,则可能不希望每次用户将get url更改为无效内容时都引发异常:[
Andrew

4
@Andrew WASD在计算机游戏角色移动中很常见。您不需要任何默认操作,因为可以将其他键分配给其他交互。在这种开关中,其很好的调用运动功能。
jemiloii '16

13
一些编译器在缺少枚举的情况下打开枚举时会发出警告,添加默认大小写会抑制该警告。考虑到这是在我的经验误差的一个非常普遍的来源(忘记更新开关的地方增加了一个枚举值后),这似乎是一个非常好的理由来省略默认情况下。
rdb

3
@virusrocks如果您的条件是枚举,则您仍然可以具有意外的值。例如,如果您向枚举添加新值,而忘记更新开关。另外,在某些语言中,您可以将整数值分配给枚举。在C ++中enum MyEnum { FOO = 0, BAR = 1 }; MyEnum value = (MyEnum)2;,生成的有效MyEnum实例不等于FOOBAR。如果这些问题中的任何一个都将导致项目错误,则默认情况下将帮助您非常快速地发现错误,通常无需调试器!
cdgraham

54

没有。

如果没有默认动作,上下文很重要。如果您只想按照一些价值观行事怎么办?

以阅读游戏按键为例

switch(a)
{
   case 'w':
     // Move Up
     break;
   case 's':
     // Move Down
     break;
   case 'a':
     // Move Left
     break;
   case 'd':
     // Move Right
     break;
}

新增:

default: // Do nothing

只是浪费时间,无缘无故地增加了代码的复杂性。


14
好的例子。但是实际上,添加带有简单// do nothing注释的default子句可以清楚地表明,如果未涵盖所有情况,则是“确定”的,与之相比,其他switch语句则是“确定的”。
钉子

8
不。编码不仅涉及处理某些情况。它还用作文档。通过编写default:并像//不做任何其他操作一样进行注释,代码的可读性变得更好。
JongAm Park

22
不同意其他意见。添加默认// //除非您不知道代码的工作方式,否则什么都不做。我可以查看没有默认值的switch语句,并且知道默认值是什么也不做。添加混乱不会使这种情况更加明显。
罗伯特·诺亚克

究竟。这与处理后期参数相同。您处理的是您关心的而不是其他的。
吉米·凯恩

2
@jybrd如果您不相信以前的开发人员有意遗漏了默认设置,那您为什么还要相信他们输入它的正确性呢?一个默认值可能是空的,就像完全丢失一样容易。对于如此琐碎的事情,无需评论。这是代码混乱。编写完整的单元测试以显示您的意图。(按我的观点)
罗伯特·诺克

45

在某些情况下,不使用默认大小写可能实际上是有益的。

如果您的切换用例是枚举值,则通过不使用默认用例,如果缺少任何用例,则可以得到编译器警告。这样,如果将来添加新的枚举值,而您又忘记在开关中添加这些值的大小写,则可以在编译时查找问题。您仍应确保代码对未处理的值采取适当的措施,以防将无效值强制转换为枚举类型。因此,这对于最简单的情况可能是最好的,在这种情况下,您可以在枚举情况下返回而不是中断。

enum SomeEnum
{
    ENUM_1,
    ENUM_2,
    // More ENUM values may be added in future
};

int foo(SomeEnum value)
{
    switch (value)
    {
    case ENUM_1:
        return 1;
    case ENUM_2:
        return 2;
    }
    // handle invalid values here
    return 0;
 }

这是重要的观察!它在Java中的应用更为广泛,因为它不允许您将整数转换为枚举。
Lii 2015年

2
在您的示例中,您可以引发异常,而不是在切换语句之后返回。这样,您既可以由编译器进行静态检查,又可以在发生意外情况时得到明确的错误。
Lii 2015年

1
我同意。在每个示例的Swift中,switch必须是详尽无遗的,否则会出现编译器错误!提供默认值只会使此有用的错误静音。另一方面,如果所有情况都得到处理,则提供默认值会引发编译器警告(无法到达的语句)。
安迪

1
刚刚修复了一个错误,如果没有默认值,该错误会导致来自编译器的警告:因此,一般来说,切换枚举变量应该没有默认值。
notan

42

无论您使用哪种语言,我都将始终使用默认子句。

事情可以而且确实会出错。价值将不是您所期望的,依此类推。

不想包含默认子句意味着您有信心知道一组可能的值。如果您认为自己知道一组可能的值,那么,如果该值超出了该组可能的值,则希望得到通知-这肯定是错误的。

这就是为什么您应该始终使用默认子句并引发错误的原因,例如在Java中:

switch (myVar) {
   case 1: ......; break;
   case 2: ......; break;
   default: throw new RuntimeException("unreachable");
}

除了“不可达”字符串外,没有其他理由需要包含更多信息。如果确实发生,则无论如何都将需要查看源和变量的值等,并且异常stacktrace将包含该行号,因此无需浪费时间在异常消息中编写更多文本。


39
我希望这样做,throw new RuntimeException("myVar invalid " + myVar);因为这样可以为您提供足够的信息,以找出并修复错误,而不必使用调试器和检查变量,如果这是很少发生且难以重现的问题,则可能会很困难。
克里斯·多德

1
如果值不符合情况之一不是错误条件怎么办?
加布

4
如果不是错误情况,则不需要default:。但是更常见的是,人们会忽略默认值,因为有人认为myVar除了列出的值之外,它永远不会有其他值。但是,当变量的值不同于其“可能”具有的值时,我无法计数在现实生活中感到惊讶的次数。在那种情况下,我一直很感谢能够让我马上看到异常的异常,而不是以后引起其他错误(更难调试)或错误答案(在测试中可能被忽略)。
Adrian Smith

2
我同意阿德里安。努力失败,尽早失败。
Slappy

我更喜欢抛出一个AssertionError而不是RuntimeException,因为这种情况与声明myVar为已处理值之一的断言非常相似。而且,有人抓住并吞下错误的机会也较小。
Philipp Wendler

15

在我的公司中,我们为航空电子和国防市场编写软件,并且始终包含默认语句,因为必须明确处理switch语句中的所有情况(即使只是说“不做任何事情”的注释)。我们不能仅仅因为行为不当或简单地因意外(甚至我们认为不可能的)价值而崩溃就买得起该软件。

可以讨论的是,默认情况并非总是必要的,但是通过始终要求它,我们的代码分析器可以轻松地检查它。


1
我在工作地点有相同的要求;我们为嵌入式微控制器编写代码,这些微控制器必须通过严格的安全检查,并且经常受到EMI的影响。假设枚举变量永远不会具有不在枚举列表中的值是不负责任的。
oosterwal

12

“ switch”语句是否应始终包含默认子句?否。通常应包含默认值。

仅当需要执行某些操作(例如断言错误条件或提供默认行为)时,才包含default子句才有意义。包含一个“仅仅因为”是对货物的狂热编程,它没有任何价值。这就是说所有“ if”语句都应包含“ else”的“开关”。

这是一个无意义的小例子:

void PrintSign(int i)
{
    switch (Math.Sign(i))
    {
    case 1:
        Console.Write("positive ");
        break;
    case -1:
        Console.Write("negative ");
        break;
    default: // useless
    }
    Console.Write("integer");
}

这等效于:

void PrintSign(int i)
{
    int sgn = Math.Sign(i);
    if (sgn == 1)
        Console.Write("positive ");
    else if (sgn == -1)
        Console.Write("negative ");
    else // also useless
    {
    }
    Console.Write("integer");
}

我不同意。恕我直言,唯一不应该存在默认值的情况是,从现在到永远没有办法更改输入集,并且您已经涵盖了所有可能的值。我能想到的最简单的情况是数据库中的布尔值,其中唯一的答案(直到SQL更改!)是true,false和NULL。在任何其他情况下,都应具有一个默认值或一个“不应发生!”的断言或例外。很有道理。如果您的代码发生更改,并且您有一个或多个新值,则可以确保为它们编写代码,否则,测试将失败。
Harper Shelby

4
@Harper:您的示例属于“声明错误条件”类别。
加布

我断言我的例子是规范,只有少数情况可以百分百确定每个可能的情况都包括在内,并且不需要默认值是例外。答案的措词听起来(至少对我来说)听起来好像什么都不做的情况会经常发生。
哈珀·谢尔比

@哈珀:好的,我更改了措辞以表示不采取行动的情况不太普遍。
加布

4
我认为default:在这些情况下,a会编纂您的假设。例如,什么时候sgn==0可以打印integer(正面或负面)都可以,还是这是一个错误?对我来说,阅读这段代码很难说。我假设您不想zero在这种情况下编写代码integer,并且程序员做出的假设sgn只能是-1或+1。如果真是这样,拥有default:可以让程序员早日发现假设错误并更改代码。
Adrian Smith

7

据我所见,答案是“默认”是可选的,说一个开关必须始终包含一个默认值,就像说每个“ if-elseif”都必须包含一个“ else”一样。如果默认情况下有逻辑要执行,那么“ default”语句应该存在,否则,代码可以继续执行而无需执行任何操作。


6

拥有的时候不是它默认的条款真正需要的是防御性编程 这通常会导致代码,是因为太多的错误处理代码过于复杂。错误处理和检测代码会损害代码的可读性,使维护工作更加困难,并最终导致超出其解决范围的错误。

因此,我认为,如果不应达到默认值,则不必添加它。

请注意,“不应该达到”是指如果达到要求,这是软件中的错误-您确实需要测试由于用户输入等原因可能包含不需要的值的值。


3
发生意外情况的另一个常见原因:可能是几年之后,修改了代码的其他部分。
Hendrik Brummermann

确实,这是非常有害的行为。至少,我会添加一个assert()子句。然后,只要有错误,就很容易检测到。
Kurt Pattyn

5

我会说这取决于语言,但是在C语言中,如果要打开枚举类型并处理所有可能的值,则最好不包括默认大小写。这样,如果以后添加一个附加的enum标签,而忘记将其添加到开关中,那么胜任的编译器将向您发出有关丢失情况的警告。


5
枚举不太可能处理“所有可能的值”。即使未明确定义该特定值,也可以将整数转换为枚举。哪个编译器警告丢失的情况?
TrueWill 2011年

1
@TrueWill:是的,您可以使用显式强制转换来编写无法理解的模糊代码;编写易于理解的代码,应该避免这种情况。 gcc -Wall(称职的编译器的最低公分母)给出有关switch语句中未处理的枚举的警告。
克里斯·多德

4

如果您知道switch语句将仅具有一组严格定义的标签或值,则只需覆盖基数即可,这样您将始终获得有效的结果。成为其他值的最佳处理程序。

switch(ResponseValue)
{
    default:
    case No:
        return false;
    case Yes;
        return true;
}

default这里是否还需要冒号?还是这样做允许某些特殊的语法让您忽略它?
Ponkadoodle

冒号是必需的,那是一个错字,谢谢您引起我的注意。
deegee

3

至少在Java中不是必需的。根据JLS的说法,它最多可以存在一个默认情况。这意味着没有默认情况是可以接受的。有时它还取决于您使用switch语句的上下文。例如,在Java中,以下开关块不需要默认大小写

private static void switch1(String name) {
    switch (name) {
    case "Monday":
        System.out.println("Monday");
        break;
    case "Tuesday":
        System.out.println("Tuesday");
        break;
    }
}

但是在以下期望返回String的方法中,默认情况很方便,可以避免编译错误

    private static String switch2(String name) {
    switch (name) {
    case "Monday":
        System.out.println("Monday");
        return name;

    case "Tuesday":
        System.out.println("Tuesday");
        return name;

    default:
        return name;
    }
}

尽管您可以通过仅在末尾使用return语句来避免上述方法的编译错误而无需使用默认大小写,但是提供默认大小写使其更具可读性。


3

有些(过时的)准则是这样说的,例如MISRA C

最终默认子句的要求是防御性编程。该条款应采取适当的措施或包含关于为何不采取任何措施的适当注释。

该建议已过时,因为它不是基于当前的相关标准。Harlan Kassler所说的是明显的遗漏:

省略默认大小写,可使编译器在看到未处理的大小写时有选择地发出警告或失败。毕竟,静态可验证性比任何动态检查都好,因此,在需要动态检查时也不值得牺牲。

正如Harlan所演示的那样,可以在切换后重新创建默认情况下的等效功能。当每种情况都是早期归还时,这是微不足道的。

从广义上讲,动态检查的典型需求是输入处理。如果值来自程序的控制范围之外,则无法信任该值。

这也是Misra采取极端防御性编程的立场,在这种情况下,只要物理上可表示无效值,就必须检查该值,无论该程序是否正确无误。如果在出现硬件错误的情况下软件需要尽可能可靠,则这是有道理的。但是正如Ophir Yoktan所说,大多数软件最好不要“处理”错误。后者的做法有时称为进攻性编程


2

您应该具有默认值以捕获未预期的值。

但是,我不同意Adrian Smith的默认错误消息应该完全没有意义。您可能无法预料到您的用户最终会看到的未处理的情况(这很重要),并且类似“ unreachable”的消息是完全没有意义的,在这种情况下不会帮助任何人。

举个例子,您有多少次完全没有意义的蓝屏死机?还是致命异常@ 0x352FBB3C32342?


例如-请记住,不仅开发人员总是会看到错误消息。我们生活在现实世界中,人们会犯错误。
约翰·亨特

2

如果开关值(switch(variable))不能达到默认情况,则根本不需要默认情况。即使我们保留默认大小写,也不会执行它。这是无效代码。


2

这是一个可选的编码“约定”。取决于用途是是否需要。我个人认为,如果您不需要它,就不应该在那里。为什么要包含用户不会使用或无法访问的内容?

如果情况可能有限(即布尔值),则默认子句是多余的


2

如果switch语句中没有默认情况,那么如果该情况在某个时间点出现,则该行为可能是不可预测的,而这在开发阶段是无法预测的。包括default案例是一个好习惯。

switch ( x ){
  case 0 : { - - - -}
  case 1 : { - - - -}
}

/* What happens if case 2 arises and there is a pointer
* initialization to be made in the cases . In such a case ,
* we can end up with a NULL dereference */

这样的做法可能导致错误,例如NULL取消引用内存泄漏以及其他类型的 严重错误

例如,我们假设每个条件都会初始化一个指针。但是,如果default应该产生大小写,并且在这种情况下我们不进行初始化,则很可能出现空指针异常。因此,建议使用defaultcase语句,即使它可能很琐碎。


2

枚举使用的开关中可能不需要默认情况。当switch包含所有值时,默认情况将不会执行。因此在这种情况下,没有必要。


1

取决于特定语言中switch的工作方式,但是在大多数语言中,如果不区分大小写,执行将通过switch语句进行,而不会发出警告。想象一下,您期望一组值并在switch中处理它们,但是在输入中得到另一个值。什么也没发生,你什么也不知道。如果您拖欠了该案,您将知道出了点问题。


1

我不同意上述Vanwaril的最高票数答案。

任何代码都会增加复杂性。还必须为此进行测试和记录。因此,如果您可以使用更少的代码进行编程,那总是好的。我的意见是,我对非穷举开关语句使用默认子句,而对穷举开关语句不使用默认子句。为了确保我做对了,我使用了静态代码分析工具。因此,让我们进入细节:

  1. 非详尽的switch语句:这些应始终具有默认值。顾名思义,这些语句并不涵盖所有可能的值。这也可能是不可能的,例如,在整数值或字符串上的switch语句。在这里,我想使用Vanwaril的示例(应该指出的是,我认为他使用了该示例提出了错误的建议。在这里我用相反的方式来表达->使用默认语句):

    switch(keystroke)
    {
      case 'w':
        // move up
      case 'a':
        // move left
      case 's':
        // move down
      case 'd':
        // move right
      default:          
        // cover all other values of the non-exhaustive switch statement
    }
    

    玩家可以按其他任何键。然后我们什么也做不了(可以通过在默认情况下添加注释就可以在代码中显示出来),或者它应该在屏幕上打印一些内容。这种情况可能会发生,因此很重要。

  2. 详尽的switch语句:这些switch语句涵盖所有可能的值,例如,关于等级系统类型枚举的switch语句。第一次开发代码时,很容易覆盖所有值。但是,由于我们是人类,因此很少有机会忘记一些东西。另外,如果以后添加枚举值,则必须对所有switch语句进行调整以使其再次穷举,这将打开通往错误地狱的路径。简单的解决方案是静态代码分析工具。该工具应检查所有switch语句,并检查它们是否详尽无遗或是否具有默认值。这里是详尽的switch语句的示例。首先我们需要一个枚举:

    public enum GradeSystemType {System1To6, SystemAToD, System0To100}
    

    然后我们需要一个像这样的枚举变量GradeSystemType type = ...。详尽的switch语句将如下所示:

    switch(type)
    {
      case GradeSystemType.System1To6:
        // do something
      case GradeSystemType.SystemAToD:
        // do something
      case GradeSystemType.System0To100:
        // do something
    }
    

    因此GradeSystemType,例如System1To3,如果将扩展为,则静态代码分析工具应检测到没有默认子句,并且switch语句不是穷举性的,因此可以保存。

只是一件事。如果我们始终使用default子句,则可能会出现静态代码分析工具无法检测穷举或非穷举switch语句的情况,因为它始终会检测该default子句。这非常糟糕,因为如果将枚举值扩展另一个值并忘记将其添加到一个switch语句中,我们将不会得到通知。


0

switch语句应始终包含默认子句吗?没有默认情况的情况下不能存在任何开关情况,在这种情况下,如果默认情况switch(x)与其他任何情况都不匹配,则默认情况将触发x的开关值。


0

我相信这是特定于语言的,对于C ++而言,枚举类类型的重要性不大。看起来比传统的C枚举更安全。但

如果您看一下std :: byte的实现,则类似:

enum class byte : unsigned char {} ;

资料来源:https : //en.cppreference.com/w/cpp/language/enum

还要考虑一下:

否则,如果T是具有固定基础类型的作用域范围或不受作用域限制的枚举类型,并且braced-init-list仅具有一个初始化器,并且从初始类型转换为基础类型的转换是非变窄的,并且初始化是直接列表初始化,然后使用将初始化程序转换为其基础类型的结果来初始化枚举。

(自C ++ 17起)

来源:https : //en.cppreference.com/w/cpp/language/list_initialization

这是枚举类的示例,代表未定义的枚举器的值。因此,您不能完全信任枚举。根据应用程序,这可能很重要。

但是,我非常喜欢@Harlan Kassler在他的帖子中所说的话,并将自己在某些情况下开始使用该策略。

只是不安全的枚举类的一个示例:

enum class Numbers : unsigned
{
    One = 1u,
    Two = 2u
};

int main()
{
    Numbers zero{ 0u };
    return 0;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.