When should one try to eliminate a switch statement?
我在我正在研究的代码库中遇到了一个switch语句,我正在尝试用更好的代码替换它,因为switch语句被认为是一种代码味道。但是,在阅读了StackOverflow上关于替换switch语句的几篇文章之后,我似乎没有想到一种有效的方法来替换这个特定的switch语句。
这让我想知道这个特定的switch语句是否正常,以及是否存在认为switch语句合适的特殊情况。
在我的例子中,我正在努力处理的代码(自然地有点模糊)如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private MyType DoSomething(IDataRecord reader) { var p = new MyType { Id = (int)reader[idIndex], Name = (string)reader[nameIndex] } switch ((string) reader[discountTypeIndex]) { case"A": p.DiscountType = DiscountType.Discountable; break; case"B": p.DiscountType = DiscountType.Loss; break; case"O": p.DiscountType = DiscountType.Other; break; } return p; } |
有人能提出一种消除这种转换的方法吗?或者这是一个适当的开关使用?如果是的话,switch语句还有其他合适的用途吗?我真的很想知道它们在哪里是合适的,所以我不会浪费太多时间试图消除我遇到的每个switch语句,只是因为它们在某些情况下被认为是一种味道。
更新:在Michael的建议下,我做了一些搜索,寻找这个逻辑的副本,发现有人在另一个类中创建了逻辑,有效地使整个switch语句冗余。因此,在这个特定代码位的上下文中,switch语句是不必要的。但是,我的问题更多的是关于代码中switch语句的适当性,以及我们是否应该总是在找到它们时尝试替换它们,所以在这种情况下,我倾向于接受这个switch语句是适当的答案。
这是一个开关站的适当使用,因为它使选择可读,并易于加或减一个。
请参见此链接。
switch语句(尤其是长语句)被认为是不好的,不是因为它们是switch语句,而是因为它们的存在表明需要重构。
switch语句的问题在于它们在代码中创建了一个分叉(就像if语句那样)。每个分支必须单独测试,每个分支内的每个分支和…好吧,你明白了。
也就是说,下面的文章在使用switch语句方面有一些好的实践:
对于您的代码,上面链接中的文章建议,如果您正在从一个枚举到另一个枚举执行这种类型的转换,您应该将开关放在自己的方法中,并使用RETURN语句而不是BREAK语句。我以前做过,代码看起来更干净:
1 2 3 4 5 6 7 8 9 | private DiscountType GetDiscountType(string discount) { switch (discount) { case"A": return DiscountType.Discountable; case"B": return DiscountType.Loss; case"O": return DiscountType.Other; } } |
我认为为了改变代码而改变代码并不是最好的利用时间。更改代码使其[更可读、更快、更高效等]是有意义的。不要仅仅因为有人说你在做"臭"的事情就改变它。
-里克
Robert Harvey和Talljoe提供了非常好的答案——这里提供的是从字符代码到枚举值的映射。最好将其表示为一个映射,其中在一个位置提供映射的详细信息,可以是在一个映射(如Talljoe建议的那样)中,也可以是在使用switch语句的函数(如Robert Harvey建议的那样)中。
在这种情况下,这两种技术可能都很好,但我想请您注意一个设计原则,它可能在这里或在其他类似的情况下有用。开放/关闭主体:
- http://en.wikipedia.org/wiki/open/closed_原则
- http://www.objectmentor.com/resources/articles/ocp.pdf(请务必阅读!)
如果映射可能会随时间变化,或者可能是扩展运行时(例如,通过插件系统或从数据库中读取映射的部分),那么使用注册表模式将帮助您遵循打开/关闭的主体,实际上允许在不影响任何使用映射的代码的情况下扩展映射(如嘿,说-打开进行扩展,关闭进行修改)。
我认为这是一篇关于注册表模式的好文章-看看注册表如何保存从某个键到某个值的映射?以这种方式,它类似于用switch语句表示的映射。当然,在您的案例中,您不会注册所有实现公共接口的对象,但是您应该获得以下要点:
- http://sinema313.wordpress.com/2009/03/01/the-registry-pattern/
因此,要回答最初的问题,case语句的格式不好,因为我希望在应用程序的多个位置需要从字符代码到枚举值的映射,因此应该将其分解出来。我提到的两个答案给了你很好的建议如何做到这一点-选择你喜欢的。但是,如果映射可能会随着时间的推移而改变,那么将注册表模式视为一种隔离代码不受这种变化影响的方法。
在我看来,气味并不是转换语句,而是它们内部的东西。对我来说,这个switch语句是可以的,直到它开始添加更多的案例。那么,可能值得创建一个查阅表格:
1 2 3 4 5 6 7 | private static Dictionary<string, DiscountType> DiscountTypeLookup = new Dictionary<string, DiscountType>(StringComparer.Ordinal) { {"A", DiscountType.Discountable}, {"B", DiscountType.Loss}, {"O", DiscountType.Other}, }; |
根据您的观点,这可能或多或少是可读的。
开始发臭的地方是如果你的箱子里的东西超过一两行。
这个switch语句很好。你们还有其他的虫子要处理吗?大声笑
不过,有一件事我注意到了…不应在IReader[]对象索引器上使用索引序号….如果列顺序发生变化怎么办?尝试使用字段名,即读卡器["id"]和读卡器["name"]
您有权怀疑这个switch语句:任何依赖于某种类型的switch语句都可能表示缺少多态性(或缺少子类)。
不过,塔尔乔的字典是一种很好的方法。
请注意,如果枚举和数据库值是整数而不是字符串,或者数据库值与枚举名称相同,则反射将起作用,例如给定
1 2 3 4 5 6 7 | public enum DiscountType : int { Unknown = 0, Discountable = 1, Loss = 2, Other = 3 } |
然后
1 2 |
就够了。
是的,这看起来像是switch语句的正确用法。
不过,我还有一个问题要问你。
为什么不包括默认标签?在默认标签中引发异常将确保在添加新的discounttypeindex并忘记修改代码时程序将正确失败。
此外,如果要将字符串值映射到枚举,则可以使用属性和反射。
类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public enum DiscountType { None, [Description("A")] Discountable, [Description("B")] Loss, [Description("O")] Other } public GetDiscountType(string discountTypeIndex) { foreach(DiscountType type in Enum.GetValues(typeof(DiscountType)) { //Implementing GetDescription should be easy. Search on Google. if(string.compare(discountTypeIndex, GetDescription(type))==0) return type; } throw new ArgumentException("DiscountTypeIndex" + discountTypeIndex +" is not valid."); } |
您的代码中是否有折扣类型的开关?添加新的折扣类型是否需要修改多个此类开关?如果是这样的话,您应该考虑将开关分解。如果没有,在这里使用开关应该是安全的。
如果在您的程序中有很多特定于折扣的行为,您可能需要像这样重构:
1 | p.Discount = DiscountFactory.Create(reader[discountTypeIndex]); |
然后,discount对象包含与计算折扣相关的所有属性和方法。
我不会用
只是为了吓唬人,这比你的代码还不清楚:
1 2 3 4 5 6 | if (string) reader[discountTypeIndex]) =="A") p.DiscountType = DiscountType.Discountable; else if (string) reader[discountTypeIndex]) =="B") p.DiscountType = DiscountType.Loss; else if (string) reader[discountTypeIndex]) =="O") p.DiscountType = DiscountType.Other; |
这个
我并不是绝对反对switch语句,但是在这里,我至少已经消除了分配discounttype的重复;相反,我可能编写了一个函数,返回给定字符串的discounttype。该函数可以简单地为每种情况使用返回语句,从而消除了中断的必要性。我发现在交换案件之间需要休息是非常危险的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private MyType DoSomething(IDataRecord reader) { var p = new MyType { Id = (int)reader[idIndex], Name = (string)reader[nameIndex] } p.DiscountType = FindDiscountType(reader[discountTypeIndex]); return p; } private DiscountType FindDiscountType (string key) { switch ((string) reader[discountTypeIndex]) { case"A": return DiscountType.Discountable; case"B": return DiscountType.Loss; case"O": return DiscountType.Other; } // handle the default case as appropriate } |
很快,我就注意到finddiscounttype()确实属于discounttype类并移动了函数。
我认为这取决于您是在创建mtype添加许多不同的地方,还是只在这个地方。如果您在许多地方创建mtype,总是需要切换到dicsount类型,并进行一些其他检查,那么这可能是一种代码味道。
我会尝试在你的程序中的一个地方创建mtype,可能是在mtype本身的构造函数中,也可能是在某种工厂方法中,但是让程序中的随机部分赋值可能会导致一些人不知道值应该是什么,并且做了一些错误的事情。
所以开关是好的,但也许开关需要在类型的创建部分内移动更多。
当你设计一种语言并最终有机会删除整个语言中最丑陋、最不直观、最容易出错的语法时。
这就是您尝试删除switch语句的时候。
只是说清楚,我指的是语法。这是从C/C++中得到的,它应该被改变以符合C语言中更现代的语法。我完全同意提供开关的概念,这样编译器就可以优化跳转。