switch语句应该总是包含一个默认子句吗?

在我的第一次代码评审中(不久前),有人告诉我,在所有switch语句中包含一个default子句是一种很好的实践。我最近想起了这个建议,但不记得理由是什么了。现在我听起来很奇怪。

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

  2. 是否依赖于语言?我不记得当时我在用什么语言了——也许这适用于一些语言而不是其他语言?

338897 次浏览

“switch”语句总是应该包含一个默认子句吗?不。通常应该包含一个默认值。

包含默认子句只有在它需要做某些事情时才有意义,比如断言错误条件或提供默认行为。包括一个“仅仅因为”是狂热的节目,没有任何价值。这相当于说所有的“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");
}

这取决于switch在特定语言中的工作方式,但是在大多数语言中,当没有匹配到case时,switch语句的执行就会毫无警告地结束。想象一下,你期望得到一组值,并在switch中处理它们,然而你在输入中得到了另一个值。什么都没发生,你也不知道什么都没发生。如果您发现了违约情况,您就会知道有问题。

我总是会使用默认子句,无论您使用哪种语言。

事情会出错,也确实会出错。价值观不会是你所期望的,等等。

不想包含默认子句意味着您确信自己知道可能的值集。如果你相信你知道这个可能值的集合,那么如果这个值在这个可能值的集合之外,你会想要知道它-这当然是一个错误。

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

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

除了“unreachable”字符串之外,没有理由包含更多信息;如果它真的发生了,无论如何您都需要查看源代码和变量的值等,异常堆栈跟踪将包括该行号,因此不需要浪费时间在异常消息中写入更多文本。

我会说这取决于语言,但在C中,如果你要打开一个枚举类型并处理每个可能的值,最好不要包含默认大小写。这样,如果您稍后添加了一个额外的enum标记,并且忘记将其添加到交换机中,称职的编译器将会就缺少的大小写向您发出警告。

开关案例几乎应该总是有一个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. 处理“默认”操作,其中的case是针对特殊行为的。

您在菜单驱动程序和bash shell脚本中经常看到这种情况。当变量声明在switch-case之外但没有初始化时,您也可能会看到这种情况,并且每个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
}

当不需要真的时使用默认子句是防御性编程 这通常导致代码过于复杂,因为有太多的错误处理代码。 这种错误处理和检测代码损害了代码的可读性,使维护更加困难,并最终导致比解决更多的错误

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

注意,“不应该达到”意味着如果达到了,这是软件中的一个错误——你确实需要测试可能包含不需要的值的值,因为用户输入等等。

在我的公司,我们为航空电子设备和国防市场编写软件,我们总是包含一个默认语句,因为switch语句中的所有情况都必须显式处理(即使它只是一个说“什么都不做”的注释)。我们不能让软件只是行为不端,或者仅仅是在意外值(甚至是我们认为不可能的值)上崩溃。

可以讨论的是,默认情况并不总是必要的,但通过总是需要它,我们的代码分析人员很容易检查它。

您应该有一个默认值来捕捉传入的非预期值。

然而,我不同意Adrian Smith关于默认的错误消息应该是完全没有意义的。可能会有一个你没有预见到的未处理的情况(这是一点),你的用户最终会看到,像“无法到达”这样的消息是完全没有意义的,在这种情况下对任何人都没有帮助。

举个例子,你有多少次经历过毫无意义的蓝皮书?或者致命异常@ 0x352FBB3C32342?

不。

如果没有默认操作,那么上下文就很重要了。如果你只关心几个价值怎么办?

以读取游戏按键为例

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

只是浪费时间,毫无理由地增加了代码的复杂性。

在我看来,答案是“default”是可选的,说一个开关必须总是包含一个默认值,就像说每个“if-elseif”必须包含一个“else”。 如果有一个逻辑在默认情况下要完成,那么'default'语句应该在那里,但否则代码可以继续执行而不做任何事情

如果你知道switch语句只有一组严格定义的标签或值,那么就这样做,以覆盖基础,这样你总是会得到有效的结果。只需将默认值放在标签之上,从编程/逻辑上来说,它将是其他值的最佳处理程序。

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

至少在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语句,但是提供默认大小写会使它更具可读性。

如果开关值(开关(变量)不能达到默认情况,则根本不需要默认情况。即使我们保留默认情况,它也不会被执行。这是死代码。

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

如果大小写可能性有限(即布尔值),则默认子句为< em > < / em >冗余!

在某些情况下,没有默认情况实际上是有益的。

如果你的开关案例是枚举值,通过没有默认案例,如果你缺少任何案例,你可以得到编译器的警告。这样,如果将来添加了新的enum值,而您忘记在交换机中为这些值添加大小写,则可以在编译时发现问题。您仍然应该确保代码对未处理的值采取适当的操作,以防将无效值强制转换为enum类型。所以这可能在简单的情况下工作得最好,你可以在枚举中返回,而不是break。

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;
}
如果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 */
这样的做法会导致类似零废弃内存泄漏以及其他类型的错误 严重缺陷< / em >。

例如,我们假设每个条件初始化一个指针。但是如果default case是 应该是上升的,如果我们不初始化这个例子,那么就完全有可能上升 一个空指针异常。因此,建议使用default case语句,即使它

一些(过时的)指南是这么说的,比如MISRA - C:

最后一个默认子句的要求是防御性编程。该条款应采取适当的行动,或包含关于为什么不采取行动的适当评论。

这个建议已经过时了,因为它不是基于目前的相关标准。明显的疏漏是哈兰·卡斯勒说的

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

正如Harlan还演示的那样,在切换之后可以重新创建与默认情况相同的功能。当每个情况都是早期返回时,这是微不足道的。

从广义上讲,动态检查的典型需求是输入处理。如果一个值来自程序控制之外,它就不可信。

这也是Misra采用极端防御性编程的的角度来看的地方,即只要一个无效值在物理上是可表示的,就必须检查它,无论程序是否可证明是正确的。如果软件需要在出现硬件错误时尽可能地可靠,这是有意义的。但正如Ophir Yoktan所说,大多数软件最好不要“处理”错误。后一种做法有时被称为进攻的编程

enum所使用的开关可能不需要默认大小写。当switch包含所有值时,默认情况将永远不会执行。所以在这种情况下,这是不必要的。

switch语句应该总是包含一个default子句吗? 如果没有缺省case,则不存在任何开关case,在缺省case中,当与任何其他case值不匹配时,将触发开关值switch(x),在此情况下x

我相信这是特定于语言的,对于c++来说是枚举类类型的一个小问题。这似乎比传统的C enum更安全。但

如果你看一下std::byte的实现,它是这样的:

enum class byte : unsigned char {} ;

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

再想想这个:

否则,如果T是一个枚举类型,它是作用域或 具有固定底层类型的无作用域,如果带括号的init-list具有 只有一个初始化式,如果从初始化式转换为 基础类型是非窄化的,如果初始化为 -直接列表初始化,然后使用 将初始化式转换为其基础类型的结果。< / p >

(因为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;
}

我不同意上面Vanwaril投票最多的回答。

任何代码都会增加复杂性。此外,还必须为此进行测试和文档编制。所以用更少的代码编程总是好的。我的观点是,我对非穷举switch语句使用default子句,而对穷举switch语句不使用default子句。为了确保我做对了,我使用了静态代码分析工具。让我们来详细了解一下:

  1. 非穷举开关语句:这些总是应该有默认值。顾名思义,这些语句不包括所有可能的值。这也可能是不可能的,例如在整数值或字符串上的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语句示例。首先,我们需要一个枚举:

    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语句中,我们将不会得到通知。