消除 `switch` 语句

200

有哪些方法可以在代码中消除switch语句的使用?


18
当适当使用时,为什么要消除开关?请详细说明您的问题。 - RB.
我投票支持将此问题设为社区可编辑,因为每个人都在说同样的事情,将所有意见汇总起来可能会很好。;) - Josh
2
Switch并不像其他指令那样标准。例如,在C++中,你很可能会忘记使用'break',然后就会得到意外的结果。此外,这个'break'与GOTO太相似了。 我从未尝试过消除switches,但肯定它们不是我最喜欢的指令。;) - anon
2
我认为他指的是switch概念,而不是C++或Java的switch语句。 switch也可以是一系列'if-else if'块的链。 - Tim Frey
2
你可能会感到惊讶:敏捷社区有许多知名程序员加入了反IF运动:http://www.antiifcampaign.com/supporters.html - Mike A
23个回答

292

Switch语句本身并不是反模式,但如果你在编写面向对象的代码时,应该考虑是否可以使用多态来代替使用switch语句。

通过多态,可以这样实现:

foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case "dog":
            echo animal.bark();
            break;

        case "cat":
            echo animal.meow();
            break;
    }
}

变成了这样:

foreach (var animal in zoo) {
    echo animal.speak();
}

2
我因为在http://stackoverflow.com/questions/374239/why-doesnt-python-have-a-switch-statement#374290中提出类似建议而受到了抨击。很多人不相信多态性 :) 这是一个非常好的例子。 - Nazgob
38
我以前从未使用过带有 typeofswitch 语句,而且这个回答也没有建议在其他情况下如何绕开 switch 语句或原因。 - Kevin
3
我同意@Kevin的观点——这个例子并没有真正展示如何通过多态性来消除switch。一些简单的例子包括:通过枚举值获取其名称,或者通过某种算法中的适当值执行某些代码。 - HotJard
我在想如何在依赖于特定实现之前消除它,例如在工厂中。我在这里找到了一个很好的例子https://dev59.com/4XA75IYBdhLWcg3wK1wp#3434505。 - juanmf
2
非常简单的例子。 - GuardianX
通过多态重构开关案例确实有助于在应用程序进一步开发和维护时提高易用性。此外,如果我们有更多的情况或先前的情况被修改,那么具有业务逻辑的现有代码也不需要更改。https://dev59.com/Tpzha4cB1Zd3GeqPCUOD#40452099 - nits.kk

264

请参考Switch Statements Smell

通常,类似的switch语句散布在程序中。如果你在一个switch中添加或删除一个子句,经常需要查找和修复其他switch语句。

重构重构到模式都有解决此问题的方法。

如果您的(伪)代码看起来像:

class RequestHandler {
    public void handleRequest(int action) {
        switch(action) {
            case LOGIN:
                doLogin();
                break;
            case LOGOUT:
                doLogout();
                break;
            case QUERY:
               doQuery();
               break;
        }
    }
}

这段代码违反了开闭原则,并且对每个新类型的动作代码都很脆弱。 为了解决这个问题,你可以引入一个“命令”对象:

interface Command {
    public void execute();
}

class LoginCommand implements Command {
    public void execute() {
        // do what doLogin() used to do
    }
}

class RequestHandler {
    private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
    public void handleRequest(int action) {
        Command command = commandMap.get(action);
        command.execute();
    }
}

7
谢谢你提供这个重构代码的绝佳示例。虽然一开始可能有点难读(因为需要在几个文件之间切换才能完全理解它)。 - rshimoda
9
只要你意识到多态解决方案牺牲了代码的简洁性,反对使用 switch 的论据就是有效的。此外,如果你总是将 switch case 存储在枚举中,一些编译器会警告你 switch 中缺少状态。 - Harvey
1
这是一个很好的例子,关于完整/不完整操作以及将代码重构为面向对象编程。非常感谢。我认为,如果面向对象编程/设计模式的支持者建议将面向对象编程概念视为运算符而不是概念,那么这将非常有用。我的意思是,“extends”、“factory”、“implements”等在文件、类、分支之间经常被使用。它们应该像“+”、“-”、“+=”、“?:”、“==”、“->”等一样简单。当程序员将它们视为简单的运算符时,他才能跨越整个类库考虑程序状态和(不)完整操作。 - namespaceform
19
我开始觉得SWITCH比这个更易理解和逻辑性更强。我通常非常喜欢OOP,但这种解决方案似乎太抽象了。 - Kaloyan Roussev
2
在使用Command对象时,生成Map<Integer,Command>的代码不需要使用switch吗? - ataulm
显示剩余4条评论

46

开关是一种模式,无论是使用switch语句、if else链、查找表、面向对象的多态性、模式匹配或其他方式实现。

您是否想消除“switch语句”或“switch模式”?第一个可以被消除,但第二个只有在能够使用另一种模式/算法时才能消除,而大多数情况下这是不可能的或不是更好的方法。

如果您想从代码中消除switch语句,首先要问的问题是在哪些情况下消除switch语句并使用其他技术是有意义的。不幸的是,这个问题的答案是特定于领域的。

请记住,编译器可以对switch语句进行各种优化。因此,例如,如果您想高效地进行消息处理,则使用switch语句就是最好的选择。但另一方面,基于switch语句运行业务规则可能不是最佳选择, 应该重新架构应用程序。

以下是一些替代switch语句的方法:


1
有人能够比较一下使用 switch 和其他替代方案进行消息处理的区别吗? - Mike A

38

Switch本身并不是很糟糕,但如果你在方法中有大量“switch”或“if/else”对对象进行操作,则可能表明您的设计有点“过程化”,并且您的对象只是值桶。将逻辑移到您的对象上,在对象上调用一个方法,让它们决定如何响应。


当然,前提是他不是在写C语言。 :) - Bernard
1
在C语言中,他可以利用函数指针和结构体来构建类似对象的东西 ;) - Tetha
你可以用任何语言编写 FORT^H^H^H^H Java。;p - Bernard
完全同意 - switch 语句是减少代码行数的好办法,但不要过度使用。 - HotJard

22

我认为最好的方法是使用一个好的Map。使用字典,您可以将几乎任何输入映射到其他值/对象/函数。

你的代码应该看起来像这样(伪代码):

void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

Object/Action DoStuff(Object key){
    return Map[key];
}

5
这取决于语言。它可能比switch语句更难读。 - Vinko Vrsalovic
这是真的,我可能不会在任何简单的情况下使用它,但它确实在配置方面提供了一定程度的灵活性,而不是像switch语句那样硬编码。可以即时准备一个字典,而switch语句则总是硬编码的。 - Josh
1
这取决于你的键。编译器可以将 switch 语句编译成简单的查找或极快的静态二进制搜索,无论哪种情况都不需要任何函数调用。 - Nick Johnson
事实上,我见过一些编译器能够识别这种映射模式并将其编译成switch语句。 :) - Tetha
@Tetha 可能是函数式语言编译器? - Mateen Ulhaq
显示剩余4条评论

14

每个人都喜欢巨大的if else块,如此易读!但我很好奇你为什么想要删除switch语句。如果你需要一个switch语句,那么你可能就需要一个switch语句。说真的,这取决于代码在做什么。如果所有switch语句都在调用函数(比如),那么可以传递函数指针。但是否更好的解决方案还有待商榷。

语言在这里也是一个重要因素,我认为。


18
我猜那是讽刺的意思 :) - Craig Day

6

我认为你需要的是策略模式。

这可以通过多种方式来实现,如其他回答中提到的:

  • 值-函数映射
  • 多态性(对象的子类型将决定它如何处理特定过程)
  • 一级函数

5
'switch'只是一种语言结构,所有的语言结构都可以被视为完成工作的工具。就像实际工具一样,有些工具更适合某些任务,而不适合其他任务(你不会用铁锤挂画)。重要的是如何定义“完成工作”。它需要可维护性、速度、可扩展性等等。
在编程过程的每个阶段,通常有一系列可以使用的结构和模式:switch,if-else-if序列,虚函数,跳转表,带有函数指针的映射等等。有经验的程序员会本能地知道在特定情况下使用正确的工具。
必须假设任何维护或审核代码的人都至少与原始作者同样熟练,以便可以安全地使用任何结构。

好的,但是为什么我们需要五种不同的、冗余的方式来完成相同的事情——条件执行? - Mike A
@mike.amy:因为每种方法都有不同的优缺点,而最重要的是以最小的代价获得最大的收益。 - Skizz

5
如果你发现自己需要添加新的状态或新的行为,使用switch语句进行替换是一个不错的选择:
int state;
String getString() { switch (state) { case 0 : // state 0 的行为 return "zero"; case 1 : // state 1 的行为 return "one"; } throw new IllegalStateException(); }
double getDouble() {
switch (this.state) { case 0 : // state 0 的行为 return 0d; case 1 : // state 1 的行为 return 1d; } throw new IllegalStateException(); }
添加新的行为需要复制switch并添加新的状态意味着在每个switch语句中添加另一个case
在Java中,您只能切换一些基本类型的值,这些值在运行时已知。这本身就是一个问题:状态被表示为魔法数字或字符。
可以使用模式匹配和多个if-else块,但在添加新行为和新状态时仍然存在相同的问题。
其他人提出的解决方案是“多态”的实例,它是状态模式的一种:
用其自己的类替换每个状态。每个行为在类上都有自己的方法:
IState state;
String getString() { return state.getString(); }
double getDouble() { return state.getDouble(); }
每次添加新状态时,您必须添加IState接口的新实现。在使用switch时,您需要向每个switch中添加一个case
每次添加新行为时,您需要向IState接口及其各个实现中添加新方法。这与以前的负担相同,但现在编译器将检查您是否在每个预先存在的状态上实现了新行为。
其他人已经说过,这可能太重了,因此当然有一个点,您可以从一个点移动到另一个点。就我个人而言,第二次编写switch语句时,我会进行重构。

4

if-else

我并不认同switch语句本质上是坏的这个前提。


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