消除代码切换的方法
消除在代码中使用switch的方法有哪些?
解决方案
每个人都喜欢巨大的"其他"区块。很容易阅读!我很好奇为什么我们要删除switch语句。如果需要switch语句,则可能需要switch语句。认真地说,我会说这取决于代码的功能。如果所有开关都在调用函数(例如),则可以传递函数指针。是否是更好的解决方案尚待商bat。
我认为语言在这里也是一个重要因素。
我认为最好的方法是使用一张好的地图。使用字典,我们可以将几乎所有输入映射到其他值/对象/功能。
代码看起来像这样(伪):
void InitMap(){ Map[key1] = Object/Action; Map[key2] = Object/Action; } Object/Action DoStuff(Object key){ return Map[key]; }
如果别的
我驳斥了切换本质上是不好的前提。
好吧,对于一个人,我不知道使用switch是一种反模式。
其次,switch总是可以用if / else if语句代替。
最明显的,与语言无关的答案是使用一系列"如果"。
如果我们使用的语言具有函数指针(C)或者具有一等值(Lua)的函数,则可以使用(指向)函数的数组(或者列表)来获得类似于"切换"的结果。
如果我们想获得更好的答案,则应该对语言更加具体。
你为什么要在一个好的编译器的手中,switch语句比if / else块更有效(并且更易于阅读),并且如果将它们替换为任何种类,则只有最大的switch可能被加速间接查找数据结构。
切换本身并没有那么糟糕,但是如果我们在方法中的对象上有很多"切换"或者" if / else",则可能表明设计有点"程序化",并且对象只是有价值桶。将逻辑移到对象上,在对象上调用一个方法,然后让他们决定如何响应。
"转换"只是一种语言结构,所有语言结构都可以视为完成工作的工具。与实际工具一样,某些工具比另一种更适合于一项任务(我们不会使用大铁锤来放置图片挂钩)。重要的部分是如何定义"完成工作"。它是否需要可维护,是否需要快速,是否需要扩展,是否需要可扩展等等。
在编程过程的每个点上,通常都可以使用一系列构造和模式:开关,if-else-if序列,虚函数,跳转表,带有函数指针的映射等。经验丰富的程序员将本能地知道在给定情况下使用的正确工具。
必须假定维护或者审查代码的任何人至少与原始作者一样熟练,以便可以安全地使用任何构造。
如果使用开关来区分各种对象,则可能会缺少一些类来精确描述这些对象,或者缺少一些虚方法。
开关是一种模式,无论是否使用switch语句,链,查找表,oop多态性,模式匹配或者其他方式实现。
我们是否要取消使用" switch语句"或者" switch模式"?仅当可以使用其他模式/算法时才可以消除第一个,而第二个则可以消除,并且在大多数情况下这是不可能的,或者不是更好的方法。
如果要从代码中删除switch语句,首先要问的问题是在什么地方消除switch语句并使用其他技术。不幸的是,这个问题的答案是特定领域的。
请记住,编译器可以进行各种优化来切换语句。因此,例如,如果我们想高效地进行消息处理,那么执行switch语句就差不多了。但是,另一方面,基于switch语句运行业务规则可能不是最佳方法,应该重新构造应用程序。
这是switch语句的一些替代方法:
- 查找表
- 多态性
- 模式匹配(特别是在函数式编程,C ++模板中使用)
switch语句通常可以用良好的OO设计代替。
例如,我们有一个Account类,并且正在使用switch语句根据帐户类型执行不同的计算。
我建议将其替换为代表不同类型帐户的许多帐户类,并全部实现一个Account接口。
这样就无需进行切换,因为我们可以将所有类型的帐户都视为相同,并且由于具有多态性,因此将为该帐户类型运行适当的计算。
开关语句本身并不是反模式,但是如果我们要编码面向对象,则应考虑通过多态性而不是使用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(); }
我认为我们正在寻找的是策略模式。
可以通过多种方式来实现此目的,对此问题的其他答案中也提到了这些方式,例如:
- 值图->函数
- 多态性。 (对象的子类型将决定其如何处理特定过程)。
- 一流的功能。
对if / else进行另一次投票。我不是大小写或者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在概念上有很大的不同,因此无法使用。
希望能回答问题。
请参阅开关语句的气味:
Typically, similar switch statements are scattered throughout a program. If you add or remove a clause in one switch, you often have to find and repair the others too.
重构和模式重构都有解决此问题的方法。
如果(伪)代码如下所示:
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(); } }
希望这可以帮助。
取决于我们为什么要替换它!
许多解释器使用'compute gotos'而不是switch语句来执行操作码。
我想念的C / C ++开关是Pascal的" in"和范围。我也希望我能打开琴弦。但是,尽管这些对于编译器而言是微不足道的,但在使用结构,迭代器和事物完成时却是艰巨的工作。因此,相反,如果只有C的switch()更灵活,我希望有很多事情可以用switch代替!
对于C ++
如果我们指的是AbstractFactory,那么我认为registerCreatorFunc(..)方法通常比要求为每个需要的"新"语句添加一个大小写要好。然后,让所有类创建并注册一个creatorFunction(..),该宏可以通过宏轻松实现(如果我敢说的话)。我相信这是许多框架都采用的通用方法。我首先在ET ++中看到它,并且我认为许多需要DECL和IMPL宏的框架都在使用它。
如果我们发现自己在语句中添加了新的状态或者新的行为,则最好替换switch
语句:
int state; String getString() { switch (state) { case 0 : // behaviour for state 0 return "zero"; case 1 : // behaviour for state 1 return "one"; } throw new IllegalStateException(); } double getDouble() { switch (this.state) { case 0 : // behaviour for state 0 return 0d; case 1 : // behaviour for state 1 return 1d; } throw new IllegalStateException(); }
添加新行为需要复制switch
,添加新状态意味着向每个switch
语句添加另一个`case'。
在Java中,我们只能切换数量非常有限的原始类型,这些原始类型在运行时知道其值。这本身就是一个问题:状态被表示为幻数或者字符。
模式匹配和多个if块可以使用,尽管在添加新行为和新状态时确实存在相同的问题。
其他人建议的"多态性"解决方案是State模式的一个实例:
将每个州替换为其自己的类。每个行为在类上都有其自己的方法:
IState state; String getString() { return state.getString(); } double getDouble() { return state.getDouble(); }
每次添加新状态时,都必须添加IState接口的新实现。在" switch"世界中,我们将为每个" switch"添加一个" case"。
每次添加新行为时,都需要向IState接口和每个实现中添加一个新方法。尽管现在编译器将检查我们是否在每个预先存在的状态上实现了新行为,但这和以前一样负担重。
其他人已经说过,这可能太重了,因此,当然有一个点可以使我们从一个移动到另一个。就个人而言,我第二次编写开关是重构的关键。
在像C这样的过程语言中,那么switch会比任何其他选择都要好。
在面向对象的语言中,几乎总是存在其他替代方法,它们可以更好地利用对象结构,尤其是多态性。
当在应用程序的多个位置出现几个非常相似的开关块时,switch语句就会出现问题,并且需要添加对新值的支持。对于开发人员来说,很常见的做法是忘记将新值的支持添加到分散在应用程序周围的一个开关块中。
通过多态,新类将替换新值,并在添加新类的过程中添加新行为。然后,可以从超类继承这些开关点处的行为,或者对其进行重写以提供新的行为,或者在超级方法为抽象方法时实现以避免编译器错误的实现。
在没有明显的多态性发生的地方,实施策略模式非常值得。
但是,如果选择是大IF ... THEN ... ELSE块,那就算了。
使用内置的switch语句不附带的语言。我想到了Perl 5.
认真地说,为什么要避免这种情况?如果我们有充分的理由避免这种情况,那为什么不简单地避免呢?
函数指针是替换庞大的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的冗余。也许我应该对此提出疑问。 :)
切换不是打破打开关闭主体的好方法。这就是我的方法。
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替代原理"。