开关语句(switch statements)是否应该始终包含默认子句?

356

在我最初的一次代码审查中,我被告知在所有 switch 语句中都包含一个默认语句是良好的实践。最近我记起了这个建议,但不记得其理由。现在这听起来相当奇怪。

  1. 是否总是包括一个默认语句有合理的理由?

  2. 这与编程语言相关吗?我不记得当时我在使用哪种语言-也许这适用于某些语言而不适用于其他语言?


14
这将在很大程度上取决于语言。 - skaffman
22个回答

342

在大多数情况下,switch语句应该始终包含一个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外声明但没有初始化,每个case将其初始化为不同的值时,您也可能会看到这种情况。在这种情况下,default需要初始化它,以便在日后访问该变量的代码不会引发错误。

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 检查某些东西时,很明显每个其他的选择都可以忽略

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

45
你关于按键的例子有些与你刚才所说的相矛盾。:| 我认为稍微解释一下会更有意义,例如:在这种情况下,你不关心默认设置,因为如果按下另一个键,你不在乎,但如果传入的变量与预期不同,我们就会关注。 - Andrew
2
同时,如果您正在检查GET参数,您可能不希望每次用户更改get URL为无效内容时都抛出异常:[ - Andrew
6
@Andrew W-A-S-D是电脑游戏中角色移动常用的按键。由于其他按键可以分配到其他交互功能,你不需要任何默认操作。在这种情况下,最好调用移动函数。 - jemiloii
26
当使用枚举类型进行 switch 分支判断时,部分编译器会提示未覆盖所有情况的警告。在添加一个默认 case 后,警告将不再出现。考虑到这种情况经常导致错误(即在枚举值添加后忘记更新相关的 switch 分支),因此省略默认 case 是一个非常好的选择。 - rdb
5
如果你的条件是一个枚举类型,仍然可能会出现意外的值。比如说,如果你向枚举类型中添加了一个新的值,但忘记更新 switch 语句。此外,在某些编程语言中,也可以将整数值赋给枚举类型。在 C++ 中,enum MyEnum { FOO = 0, BAR = 1 }; MyEnum value = (MyEnum)2; 会创建一个有效的 MyEnum 实例,但它既不等于 FOO 也不等于 BAR。如果这两个问题中的任何一个会导致你的项目出现错误,那么一个默认情况将帮助你快速找到错误,通常无需使用调试器! - cdgraham
显示剩余3条评论

84

没有默认行为时,上下文很重要。如果您只关心少数值该怎么办?以读取游戏按键为例。

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

仅仅是浪费时间,毫无必要地增加了代码的复杂度。


29
不错的例子。但事实上,添加一个默认子句并使用简单的// do nothing注释可以清楚地表明,如果没有涵盖所有情况,那么这是“可以”的,与其他switch语句不同,在其他switch语句中这将是“不可以”的。 - The Nail
20
编码不仅仅是处理一些特定情况,它也是一种文档。通过编写“default:”和像“// 不做任何事情或者其他操作”的注释,代码的可读性会更好。 - JongAm Park
43
不同意其他评论。添加默认值// Do nothing除非你不知道代码怎么工作,否则几乎没有任何帮助。我可以查看一个没有默认值的switch语句,并知道默认操作是什么。添加杂物并不能使这更加明显。 - Robert Noack
8
为了回答您的问题,我会尽可能简明扼要地翻译。以下是需要翻译的内容:在回答的精神下,您可以删除最后一个换行符。 - Gil Vegliach
5
如果你不相信之前的开发者有意删掉了默认值,那么为什么要相信他们在添加默认值时是正确的呢?一个默认值可能会因为疏忽而被清空,就像完全没有一样容易。对于如此微不足道的事情,注释并不是必须的。这只是代码凌乱。相反,编写全面的单元测试来展示你的意图更加重要。(仅代表个人观点) - Robert Noack
显示剩余6条评论

76

在某些情况下,不设置默认情况实际上可能是有益的。

如果你的switch cases是枚举值,那么如果没有默认情况,当你缺少任何一个case时,你可以得到编译器警告。这样,如果将来添加了新的枚举值,并且你忘记为这些值添加case,你可以在编译时找到问题。但是,你仍然应该确保代码采取适当的行动来处理未经处理的值,以防将无效值转换为枚举类型。因此,对于可以在枚举情况内返回而不是中断的简单情况,这种方法可能最有效。

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;
 }

1
这是一个重要的观察!在Java中更加适用,因为它不允许将整数强制转换为枚举类型。 - Lii
3
在您的示例中,您可以在switch语句后抛出异常,而不是返回值。这样,您既可以通过编译器进行静态检查,又可以在发生意外情况时获得清晰的错误提示。 - Lii
2
我同意。例如在Swift中,switch语句必须是穷尽的,否则会出现编译器错误!提供一个默认选项只会使这个有用的错误静音。另一方面,如果所有情况都已处理,则提供默认值实际上会引发编译器警告(无法到达的语句)。 - Andy
4
刚刚修复了一个错误,如果枚举变量没有默认项,编译器会发出警告,所以一般在枚举变量的switch语句中不需要加上默认项。 - notan
我最近一直在研究Rust,它和Swift提到的行为非常相似。我想:这太酷了,C++从来没有检查过我是否已经处理好了。但实际上它确实进行了检查。在GCC中,如果使用了“-Wall”(几乎每次都是这样),则“-Wswitch”就会开启。它从未帮助过我,因为我们公司有一个强制性编码规则,规定您必须在每个switch语句中添加default,并且每个人都盲目地遵循它。 - Machta

46

无论你使用哪种编程语言,我都建议使用默认子句。

问题可能发生,值可能不是你期望的那样。

如果不想包含默认子句,这意味着你相信自己了解可能的值集合。如果值在这个可能的值集合之外,你肯定希望得到通知 - 这肯定是一个错误。

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

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

没有必要在异常消息中包含比“不可达”字符串更多的信息;如果这确实发生了,你需要查看源代码和变量值等信息,并且异常堆栈跟踪将包含该行号,因此没有必要浪费时间编写更多的文本到异常消息中。


43
我更喜欢使用throw new RuntimeException("myVar invalid " + myVar);,因为这样可以提供足够的信息让你找出并修复错误,而无需使用调试器和检查变量,如果这是一个难以重现的罕见问题,那么这可能会很困难。 - Chris Dodd
2
如果值不匹配任何一个情况,这是否算作错误条件? - Gabe
5
如果不是错误条件,那么就不需要 default:。但更常见的情况是省略了 default,因为人们认为 myVar 只可能有列出的值中的任何一个,然而在现实生活中我已经数不清有多少次变量具有不同于“可能”的这些值的值,这时我会感激这个异常使我立即看到问题,而不是导致其他更难调试的错误或错误答案(可能在测试中被忽视)。 - Adrian Smith
3
我同意Adrian的观点。失败得彻底且尽早失败。 - Slappy
我同意这种方法。只是想知道如果枚举类型有一个默认处理器,而其中每个值都有一个case的情况下会发生什么?编译器是否会将其删除?实际上,考虑一下,大多数语言允许枚举类型保存任意值,这是你绝对需要了解的。 - Robin Salih
显示剩余4条评论

18

“switch”语句是否总是需要包含一个default子句?不是。通常情况下应该包含一个default子句。

只有存在某些逻辑需求时,例如断言错误条件或提供默认行为时,才有必要包含default子句。因为仅仅为了“保险”而添加default子句是一种无效的编程方法,没有任何价值。这相当于说所有“if”语句都应该包含“else”,这是模拟程序设计的做法。

以下是一个简单的例子,说明在这种情况下没有必要使用default子句:

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");
}

1
我不同意。在我看来,唯一不应该存在默认值的情况是,如果现在和将来输入集合不可能改变,并且你已经涵盖了所有可能的值。我能想到的最简单的情况是从数据库中获取布尔值,其中唯一的答案(直到 SQL 改变!)是 true、false 和 NULL。在任何其他情况下,使用一个带有“不应该发生!”断言或异常的默认值是很有道理的。如果你的代码发生了变化,并且你有一个或多个新值,那么你可以确保为它们编写代码,如果你没有这样做,你的测试将会失败。 - Harper Shelby
6
@Harper:你的例子属于“断言错误条件”的类别。 - Gabe
我要断言我的例子是正常情况,而那些可以100%确定每种情况都被覆盖且不需要默认值的情况非常少。你的回答措辞让人觉得(至少对我来说)什么都不做的默认情况会更加普遍。 - Harper Shelby
@Harper:好的,我已经修改了措辞以表明“什么都不做”的情况较少见。 - Gabe
5
我认为在这些情况下使用 default: 可以表达你的假设。例如,当 sgn==0 时打印 integer (既非正数也非负数)是否合适?对我来说,阅读这段代码很困难。我会假设在这种情况下应该写 zero 而不是 integer,并且程序员假设 sgn 只能是 -1 或 +1。如果是这种情况,有了 default: 将允许程序员尽早捕获假设错误并更改代码。 - Adrian Smith

16
在我们公司,我们为航空电子和国防市场编写软件,并且总是包括一个默认语句,因为在 switch 语句中 ALL cases 必须明确处理(即使只是一个注释说“不做任何事情”)。 我们不能承受软件因意外(甚至是我们认为不可能的值)而出现问题或崩溃的代价。
可以讨论是否始终需要 default case,但始终要求它,这样我们的代码分析器就可以轻松地检查它。

1
我在工作中也有同样的要求;我们为嵌入式微控制器编写代码,这些微控制器必须通过严格的安全检查,并经常受到电磁干扰的影响。假设枚举变量永远不会具有枚举列表中没有的值是不负责任的。 - oosterwal
1
请参考Harlan Kassler的答案,了解为什么这是一个愚蠢的策略,实际上会隐藏错误。当然,他的答案假设有一个体面的编译器,这并不总是一件确定的事情。 - Slava
@Slava 这只是航空电子和国防领域的要求。不要说那是愚蠢的。虽然编译器在这个领域已经有了很大的进步,但在这种情况下,你需要证明你的编译器确实能够完美地检测到这些情况。请记住,这是关系到生命安全的软件。 - Kurt Pattyn
2
我曾经在航空电子和国防领域工作过。这些领域充斥着一些愚蠢的要求,对实际安全毫无贡献。特别是Misra指南(懒得找链接了),最多只能证明减少错误频率和严重程度方面是无用的。为了避免误解:我并不是说所有这些要求都是愚蠢的。大多数不是。但是有一些(而且比例不可忽略)是愚蠢的。 - Slava

8
据我所见,答案是“default”是可选的。说一个switch语句必须始终包含一个default就像说每个if-elseif语句都必须包含一个else一样。如果有默认逻辑需要执行,则应该使用“default”语句,否则代码可以继续执行而不做任何操作。

7

我不同意上面Vanwaril所提供的最受欢迎的答案。

任何代码都会增加复杂性。同时,必须对其进行测试和文档编写。因此,如果您可以使用更少的代码进行编程,则始终是好的。我的观点是,我在非穷尽性switch语句中使用默认子句,而在穷尽性switch语句中不使用默认子句。为确保我做得正确,我使用静态代码分析工具。现在让我们深入了解:

  1. Nonexhaustive switch statements: Those should always have a default value. As the name suggests those are statements which do not cover all possible values. This also might not be possible, e.g. a switch statement on an integer value or on a String. Here I would like to use the example of Vanwaril (It should be mentioned that I think he used this example to make a wrong suggestion. I use it here to state the opposite --> Use a default statement):

    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
    }
    

    The player could press any other key. Then we could not do anything (this can be shown in the code just by adding a comment to the default case) or it should for example print something on the screen. This case is relevant as it may happen.

  2. Exhaustive switch statements: Those switch statements cover all possible values, e.g. a switch statement on an enumeration of grade system types. When developing code the first time it is easy to cover all values. However, as we are humans there is a small chance to forget some. Additionally if you add an enum value later such that all switch statements have to be adapted to make them exhaustive again opens the path to error hell. The simple solution is a static code analysis tool. The tool should check all switch statements and check if they are exhaustive or if they have a default value. Here an example for an exhaustive switch statement. First we need an enum:

    public enum GradeSystemType {System1To6, SystemAToD, System0To100}
    

    Then we need a variable of this enum like GradeSystemType type = .... An exhaustive switch statement would then look like this:

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

    So if we extend the GradeSystemType by for example System1To3 the static code analysis tool should detect that there is no default clause and the switch statement is not exhaustive so we are save.

还有一件事情,如果我们总是使用default子句,可能会发生静态代码分析工具无法检测到完整或非完整的开关语句的情况,因为它总是检测default子句。这非常糟糕,因为如果我们通过添加另一个值来扩展枚举并忘记将其添加到一个开关语句中,我们将不会得到通知。


6
当不需要默认子句时,添加默认子句是防御性编程。这通常会导致代码过于复杂,因为有太多的错误处理代码。这些错误处理和检测代码会影响代码的可读性,使维护更加困难,并最终导致比解决更多的错误。
因此,我认为如果不应该到达默认值,则无需添加它。
请注意,“不应该到达”意味着如果到达了它,那么这是软件中的错误 - 您确实需要测试可能包含由于用户输入等原因而包含不想要的值的值。

3
出现意外情况的另一个常见原因是代码的其他部分被修改,可能是几年之后。 - Hendrik Brummermann
确实,这是非常危险的行为。至少,我会添加一个assert()语句。这样每当出现错误时就可以轻松检测到它。 - Kurt Pattyn

5

我认为这取决于编程语言,但在C语言中,如果你在枚举类型上进行切换并处理了每个可能的值,最好不要包括默认情况。这样,如果您稍后添加了一个额外的枚举标签并忘记将其添加到switch语句中,一个能力强的编译器会警告您缺少的情况。


5
用枚举可能无法处理每个可能的值。即使该特定值没有被明确定义,你也可以将整数转换为枚举类型。哪个编译器会警告缺少的情况? - TrueWill
1
@TrueWill:是的,您可以使用显式强制转换编写无法理解的混淆代码;但是,如果要编写可理解的代码,应避免这种情况。gcc -Wall(算是能够胜任的编译器最低公共分母)会警告switch语句中未处理的枚举。 - Chris Dodd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接