消除代码切换的方法


175

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


18
如果使用得当,为什么还要消除开关?请您详细说明您的问题。
RB。

我投票将这个问题设为可编辑,因为每个人都在说同样的事情,巩固所有观点可能会很好;)
乔什(Josh

2
开关不如其他说明标准。例如,在C ++中,您可能会忘记“中断”,然后会有意外的结果。此外,此“中断”与GOTO太相似。我从未尝试消除开关,但是可以肯定的是,它们并不是我最喜欢的说明。;)

2
我认为他指的是转换概念,而不是C ++或Java转换语句。开关也可以是“ if-else if”块的链。
Outlaw程序员

2
您可能会感到很有趣:敏捷社区中有大量著名的程序员参与了反IF运动:antiifcampaign.com/supporters.html
Mike A,2009年

Answers:


267

开关语句本身并不是反模式,但是如果您要编码面向对象,则应考虑使用多态性而不是使用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
我由于stackoverflow.com/questions/374239/中的类似建议而感到沮丧,因此很多ppl不相信多态:)非常好的例子。
纳兹哥布

30
-1:我从未将switch语句与一起使用typeof,并且此答案没有建议在其他情况下解决switch语句的方法或原因。
凯文2013年

3
我同意@Kevin-给出的示例并未真正说明如何通过多态性消除切换。简单的示例:通过切换其值获取枚举的名称,或在某种算法中通过适当的值执行一些代码。
HotJard

我想知道如何在依赖特定实现之前(例如在工厂中)消除它。我在这里找到了一个很好的例子stackoverflow.com/a/3434505/711855
juanmf '02

1
非常琐碎的例子。
GuardianX

239

请参阅切换语句的气味

通常,类似的switch语句分散在整个程序中。如果在一个开关中添加或删除子句,则通常也必须查找并修复其他子句。

这两种重构重构,以模式必须解决这个办法。

如果您的(伪)代码如下所示:

class RequestHandler {

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

该代码违反了开放式封闭原则,并且对随之而来的每种新型操作代码都非常脆弱。为了解决这个问题,您可以引入一个“ Command”对象:

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

如果您的(伪)代码如下所示:

class House {
    private int state;

    public void enter() {
        switch (state) {
            case INSIDE:
                throw new Exception("Cannot enter. Already inside");
            case OUTSIDE:
                 state = INSIDE;
                 ...
                 break;
         }
    }
    public void exit() {
        switch (state) {
            case INSIDE:
                state = OUTSIDE;
                ...
                break;
            case OUTSIDE:
                throw new Exception("Cannot leave. Already outside");
        }
    }

然后,您可以引入一个“状态”对象。

// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
    public HouseState enter() {
        throw new Exception("Cannot enter");
    }
    public HouseState leave() {
        throw new Exception("Cannot leave");
    }
}

class Inside extends HouseState {
    public HouseState leave() {
        return new Outside();
    }
}

class Outside extends HouseState {
    public HouseState enter() {
        return new Inside();
    }
}

class House {
    private HouseState state;
    public void enter() {
        this.state = this.state.enter();
    }
    public void leave() {
        this.state = this.state.leave();
    }
}

希望这可以帮助。


7
感谢您提供有关如何重构代码的出色示例。尽管我可能会在开始时说它有点难以阅读(因为必须在多个文件之间切换才能完全理解它)
rshimoda

8
只要您意识到多态解决方案牺牲了代码的简单性,反对switch的参数就有效。另外,如果您始终将开关用例存储在枚举中,则某些编译器会警告您开关中缺少状态。
哈维

1
这是一个关于完成/未完成操作以及将代码重构为OOP的很好的例子。万分感谢。我认为,如果OOP /设计模式的支持者建议将OOP概念像运算符而不是概念一样对待。我的意思是在文件,类,分支之间经常使用“扩展”,“工厂”,“实现”等。它们应该像“ +”,“-”,“ + =“,“?:”,“ ==”,“->””之类的运算符一样简单。当程序员在脑海中像运算符一样简单地使用它们时,然后他可以在整个类库中考虑程序状态和(完成)操作。
namespaceform

13
与此相比,我开始认为SWITCH更加可理解和合乎逻辑。我平时喜欢OOP非常多,但这项决议似乎对我来说太抽象
JK

1
使用Command对象,生成的代码Map<Integer, Command>不需要切换吗?
ataulm

41

开关是一种模式,无论是否使用switch语句,链,查找表,oop多态性,模式匹配或其他方式实现。

您是否要取消使用“ switch语句 ”或“ switch模式 ”?仅当可以使用其他模式/算法时,才可以消除第一个,而第二个则可以消除,并且在大多数情况下这是不可能的,或者不是更好的方法。

如果要从代码中删除switch语句,首先要问的问题是在什么地方消除switch语句并使用其他技术。不幸的是,这个问题的答案是特定领域的。

请记住,编译器可以进行各种优化来切换语句。因此,例如,如果您想高效地进行消息处理,那么执行switch语句就差不多了。但是,另一方面,基于switch语句运行业务规则可能不是最佳方法,应重新构造应用程序。

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


1
有人可以比较使用switch和其他方法的消息处理吗?
Mike A

37

切换本身并没有那么糟糕,但是如果您在方法中的对象上有很多“切换”或“ if / else”,则可能表明您的设计有点“程序化”,并且您的对象只是有价值的桶。将逻辑移到对象上,在对象上调用方法,然后让他们决定如何响应。


当然,假设他不是在用C. :)
Bernard

1
在C语言中,他可以(ab?)使用函数指针和结构来构建类似对象的东西;)
Tetha

您可以使用任何语言编写FORT ^ H ^ H ^ H ^ H Java。; p
Bernard

完全同意-切换是减少代码行的好方法,但不要太过用它。
HotJard

21

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

您的代码看起来像这样(伪):

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

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

4
取决于语言。与开关相比,它的可读性要差得多
Vinko Vrsalovic

在正确的情况下,这是一个不错的紧急解决方案。我最近做了这个映射键码,对于这个目的,它看起来还不错。
伯纳德

的确如此,我可能不会在简单的事情上使用它,但是在switch语句的配置方面,它确实提供了一定的灵活性。字典可以随时准备,而开关始终是硬编码的。
乔什

在某些情况下可以更清洁。也较慢,因为它需要函数调用。
尼克·约翰逊

1
那取决于你的钥匙。无论哪种情况,编译器都可以将switch语句编译为简单的查找或非常快速的静态二进制搜索,而无需任何函数调用。
尼克·约翰逊

12

每个人都喜欢巨大的if else积木。很容易阅读!我很好奇为什么您要删除switch语句。如果需要switch语句,则可能需要switch语句。认真地说,我会说这取决于代码的作用。如果所有开关都在调用函数(例如),则可以传递函数指针。这是否是更好的解决方案尚待商.。

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


13
我以为这是讽刺:)
克雷格日

6

我认为您正在寻找的是策略模式。

此问题可以通过多种方式来实现,对此问题的其他答案中也提到了这些方式,例如:

  • 值图->函数
  • 多态性。(对象的子类型将决定其如何处理特定过程)。
  • 一流的功能。

5

switch 如果您发现自己在语句中添加了新状态或新行为,则最好替换这些语句:

int状态

字符串getString(){
   开关(状态){
     案例0://状态0的行为
           返回“零”;
     情况1://状态1的行为
           返回“一个”;
   }
   抛出新的IllegalStateException();
}

double getDouble(){

   切换(this.state){
     案例0://状态0的行为
           返回0d;
     情况1://状态1的行为
           返回1天;
   }
   抛出新的IllegalStateException();
}

添加新的行为需要复制switch,并增加新的状态意味着添加另一case每一个 switch发言。

在Java中,您只能切换数量非常有限的原始类型,这些原始类型在运行时知道其值。这本身就是一个问题:状态被表示为幻数或字符。

if - else可以使用模式匹配和多个块,尽管在添加新行为和新状态时确实存在相同的问题。

其他人建议的“多态性”解决方案是State模式的一个实例:

将每个州替换为其自己的类。每个行为在类上都有自己的方法:

IState状态;

字符串getString(){
   返回state.getString();
}

double getDouble(){
   返回state.getDouble();
}

每次添加新状态时,都必须添加IState接口的新实现。在一个switch世界中,您将为case每个添加一个switch

每次添加新行为时,都需要向IState接口和每个实现中添加新方法。尽管现在编译器将检查您是否在每个预先存在的状态上实现了新行为,但这和以前一样负担重。

其他人已经说过,这可能太重了,因此,当然有一个点,您可以从一个位置移到另一个位置。就个人而言,我第二次编写开关是重构的关键。



3

好吧,有一个,我不知道使用switch是一种反模式。

其次,switch总是可以用if / else if语句替换。


恰好-开关只是一堆if / elsifs的句法美沙酮。
Mike A

3

你为什么要 在一个好的编译器的手中,switch语句比if / else块要有效得多(并且更易于阅读),并且如果将它们替换为任何种类,则只有最大的switch可能被加速。间接查找数据结构。


2
此时,您要对编译器进行第二次猜测,并根据编译器内部进行设计更改。设计应遵循问题的性质,而不是编译器的性质。
Mike A

3

“切换”只是一种语言结构,所有语言结构都可以视为完成工作的工具。与实际工具一样,某些工具比另一种更适合于一项任务(您不会使用大铁锤来放置图片挂钩)。重要的部分是如何定义“完成工作”。它是否需要维护,是否需要快速,是否需要扩展,是否需要可扩展等等。

在编程过程的每个点上,通常都可以使用一系列构造和模式:开关,if-else-if序列,虚函数,跳转表,带有函数指针的映射等。经验丰富的程序员将本能地知道在给定情况下使用的正确工具。

必须假定维护或审阅代码的任何人至少与原始作者一样熟练,以便可以安全地使用任何构造。


好的,但是为什么我们需要5种不同的冗余方式来做同一件事-条件执行?
Mike A

@ mike.amy:因为每种方法都有不同的收益和成本,而这一切都是为了以最少的成本获得最大的收益。
Skizz

1

如果使用开关来区分各种对象,则可能会缺少一些类来精确描述这些对象,或者缺少一些虚方法。


1

对于C ++

如果您指的是AbstractFactory,那么我认为registerCreatorFunc(..)方法通常比要求为每个需要的“新”语句添加一个大小写要好。然后,让所有类创建并注册一个creatorFunction(..),该函数可以使用宏轻松实现(如果我敢说的话)。我相信这是许多框架都采用的通用方法。我首先在ET ++中看到它,并且我认为许多需要DECL和IMPL宏的框架都在使用它。


1

在像C这样的过程语言中,那么switch会比任何其他选择都要好。

在面向对象的语言中,几乎总是有其他替代方法可以更好地利用对象结构,尤其是多态。

当在应用程序中的多个位置出现多个非常相似的开关块时,switch语句就会出现问题,并且需要添加对新值的支持。对于开发人员来说,很常见的做法是忘记将新值的支持添加到分散在应用程序周围的一个开关块中。

通过多态,新类将替换新值,并且新行为将作为添加新类的一部分进行添加。然后,可以从超类继承这些开关点处的行为,或者对其进行重写以提供新的行为,或者在超级方法抽象时实现该功能以避免编译器错误。

在没有明显的多态性发生的地方,很值得实施Strategy模式

但是,如果您的选择是大IF ... THEN ... ELSE块,那么就算了。


1

使用内置的switch语句不附带的语言。我想到了Perl 5。

认真地说,为什么要避免这种情况?如果您有充分的理由避免这种情况,那为什么不简单地避免呢?


1

函数指针是替换庞大的switch语句的一种方式,它们在语言中尤其有用,您可以在其中按函数名称捕获函数并使用它们进行填充。

当然,您不应该强行将switch语句从您的代码中删除,并且总是有可能您将所有操作都做错了,这导致了愚蠢的冗余代码段。(有时这是不可避免的,但是良好的语言应该可以使您在保持干净的同时消除冗余。)

这是一个很好的分而治之的例子:

假设您有某种口译员。

switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

相反,您可以使用以下代码:

opcode_table[*IP](*IP, vm);

... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };

OpcodeFuncPtr opcode_table[256] = {
    ...
    opcode_add,
    opcode_not_zero,
    opcode_jump,
    opcode_default,
    opcode_default,
    ... // etc.
};

请注意,我不知道如何在C中删除opcode_table的冗余。也许我应该对此提出疑问。:)


0

最明显的,与语言无关的答案是使用一系列“如果”。

如果您使用的语言具有函数指针(C)或具有一等值(Lua)的函数,则可以使用(指向)函数的数组(或列表)来实现类似于“切换”的结果。

如果您想获得更好的答案,则应该对语言更加具体。


0

switch语句通常可以用良好的OO设计代替。

例如,您有一个Account类,并且正在使用switch语句根据帐户类型执行不同的计算。

我建议将其替换为代表不同类型帐户的许多帐户类,并全部实现一个Account接口。

这样就无需进行切换,因为您可以将所有类型的帐户视为相同,并且由于多态性,将为该帐户类型运行适当的计算。


0

取决于您为什么要替换它!

许多解释器使用“ compute gotos”而不是switch语句来执行操作码。

我想念的C / C ++开关是Pascal的“ in”和范围。我也希望我能打开琴弦。但是,尽管这些对于编译器来说是微不足道的,但在使用结构,迭代器和事物完成时却是艰巨的工作。因此,相反,如果只有C的switch()更灵活,我希望有很多事情可以用switch代替!


0

切换不是一个好方法,因为它破坏了打开关闭委托人。这就是我的方法。

public class Animal
{
       public abstract void Speak();
}


public class Dog : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Hao Hao");
   }
}

public class Cat : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Meauuuu");
   }
}

这是如何使用它(获取您的代码):

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

基本上,我们正在做的是将责任委托给孩子,而不是让父母决定如何对待孩子。

您可能还想阅读“ Liskov替代原理”。


0

在使用关联数组的JavaScript中

function getItemPricing(customer, item) {
    switch (customer.type) {
        // VIPs are awesome. Give them 50% off.
        case 'VIP':
            return item.price * item.quantity * 0.50;

            // Preferred customers are no VIPs, but they still get 25% off.
        case 'Preferred':
            return item.price * item.quantity * 0.75;

            // No discount for other customers.
        case 'Regular':
        case
        default:
            return item.price * item.quantity;
    }
}

变成这个:

function getItemPricing(customer, item) {
var pricing = {
    'VIP': function(item) {
        return item.price * item.quantity * 0.50;
    },
    'Preferred': function(item) {
        if (item.price <= 100.0)
            return item.price * item.quantity * 0.75;

        // Else
        return item.price * item.quantity;
    },
    'Regular': function(item) {
        return item.price * item.quantity;
    }
};

    if (pricing[customer.type])
        return pricing[customer.type](item);
    else
        return pricing.Regular(item);
}

礼貌


-12

如果/否则则再次投票。我不是大小写或switch语句的忠实拥护者,因为有些人不使用它们。如果使用大小写或开关,则代码的可读性较差。也许您对它的可读性不是那么低,但是对于那些从来不需要使用该命令的人来说,可读性就不那么高。

对象工厂也是如此。

if / else块是每个人都能得到的简单构造。您可以采取一些措施来确保您不会引起问题。

首先-不要尝试多次缩进语句。如果您发现自己缩进,那么您做错了。

 if a = 1 then 
     do something else 
     if a = 2 then 
         do something else
     else 
         if a = 3 then 
             do the last thing
         endif
     endif 
  endif

真的很糟糕-改为这样做。

if a = 1 then 
   do something
endif 
if a = 2 then 
   do something else
endif 
if a = 3 then 
   do something more
endif 

优化该死。它对代码速度没有太大的影响。

其次,只要在特定代码块中散布了足够的breaks语句以使其明显,我就不反对打破If块

procedure processA(a:int)
    if a = 1 then 
       do something
       procedure_return
    endif 
    if a = 2 then 
       do something else
       procedure_return
    endif 
    if a = 3 then 
       do something more
       procedure_return
    endif 
end_procedure

编辑:在Switch上,为什么我觉得很难理解:

这是switch语句的示例...

private void doLog(LogLevel logLevel, String msg) {
   String prefix;
   switch (logLevel) {
     case INFO:
       prefix = "INFO";
       break;
     case WARN:
       prefix = "WARN";
       break;
     case ERROR:
       prefix = "ERROR";
       break;
     default:
       throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
   }
   System.out.println(String.format("%s: %s", prefix, msg));
 }

对我而言,这里的问题是适用于C语言的普通控制结构已被完全破坏。有一条通用规则,如果要在控件结构中放置多行代码,则应使用花括号或begin / end语句。

例如

for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}

对于我来说(如果我错了,您可以纠正我),Case语句将此规则抛到了窗外。有条件执行的代码块未放置在begin / end结构内。因此,我认为Case在概念上有很大不同,因此无法使用。

希望能回答您的问题。


哇-显然是有争议的答案。我会对知道自己做错了什么感兴趣。
seanyboy

嗯,切换太复杂了?我不知道...在我看来,您可以使用的语言功能就不多了。:)另外,在您的中心示例中,如果(a == 1 || a == 2 || a == 3)做某件事不是更明智吗?
拉尔斯·韦斯特格伦

同样,在您的最后一个示例中,“ break”在大多数语言中都不会执行任何操作,它会跳出最近的块(通常是循环),在您的情况下,它总是发生在下一行(endif)上。如果您使用中断是“返回”的语言,那么很多返回也将被禁止(“后卫声明”除外)
Lars Westergren 08/09/24

4
“该死的是优化。它对代码速度没有太大的影响。” 我的代码在移动平台上运行。优化很重要。此外,如果使用正确,则开关可使代码看起来非常干净(与if..elseif..elseif..elseif ...相比)。没见过他们吗?学习他们。
斯瓦蒂

切换一点也不复杂,但是我一直试图尽量减少代码中使用的构造数,以减少理解上的摩擦。
seanyboy
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.