
近50年来,switch语句(也称为case语句)一直是编程的重要组成部分。然而近年来,一些人声称switch语句已经过时了。还有人更进一步,将switch语句标记为代码异味。
1952年,Stephen Kleene在他的论文《元数学导论》中构思了switch语句。第一个值得注意的实现是在1958年的ALGOL 58中。后来,switch语句被包含在不朽的C编程语言中,众所周知,C语言影响了大多数现代编程语言。
快进到今天,几乎每种语言都有switch语句。然而,少数语言省略了switch语句。最值得注意的是Smalltalk。
这引起了我的好奇心,为什么Smalltalk排除了switch语句?
Andy Bower,Dolphin Smalltalk的创造者/支持者之一,分享了他对Smalltalk排除switch语句原因的看法:
当我第一次从C++转向Smalltalk时,我无法理解一个据说功能齐全的语言怎么不支持switch/case结构。毕竟,当我第一次从BASIC转向”结构化编程”时,我认为switch是自切片面包以来最好的东西之一。然而,因为Smalltalk不支持switch,我必须寻找并理解如何克服这个缺陷。正确的答案当然是使用多态性,让对象本身分派到正确的代码片段。然后我意识到这根本不是”缺陷”,而是Smalltalk强迫我进行比我在C++中习惯的更细粒度的面向对象设计。如果有switch语句可用,我学习这个会花费更长时间,或者更糟的是,我可能仍然在Smalltalk中编写C++/Java伪对象风格的程序。
我认为在正常的面向对象编程中,实际上不需要switch语句。有时,当与非面向对象的世界接口时(比如接收和分派不是对象而只是整数的WM_XXXX Windows消息),switch语句会很有用。在这些情况下,有替代方案(比如从Dictionary分派),它们出现的次数不足以保证包含额外的语法。
Andy说得对吗?没有switch语句我们会更好吗?其他语言也会从排除switch语句中受益吗?
为了阐明这个问题,我整理了switch语句、字典和多态性之间的比较。让我们称之为对决。愿最佳实现获胜!
每个实现都有一个接受一个参数(整数)并返回字符串的方法。我们将使用圈复杂度和可维护性指数来检查每个实现。然后我们将对所有三个实现进行整体审视。
代码如下。
Switch 语句
可维护性指数 | 72 |
---|---|
圈复杂度 | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
字典
可维护性指数 | 73 |
---|---|
圈复杂度 | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
多态性
总可维护性指数 | 94 |
---|---|
总圈复杂度 | 15 |
接口
可维护性指数 | 100 |
---|---|
圈复杂度 | 1 |
public interface IColor
{
string ColorName { get; }
}
工厂
可维护性指数 | 76 |
---|---|
圈复杂度 | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
实现
可维护性指数 | 97 |
---|---|
圈复杂度 | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
结果
在深入结果之前,让我们定义圈复杂度和可维护性指数:
- 圈复杂度是逻辑分支的度量。数字越低越好。
- 可维护性指数衡量代码的可维护性。它的范围在0到100之间。数字越高越好。
圈复杂度 | 可维护性指数 | |
---|---|---|
Switch 语句 | 6 | 72 |
字典 | 3 | 73 |
多态性 | 15 | 94 |
我们将首先检查圈复杂度。
圈复杂度的结果很直接。字典实现是最简单的。这意味着它是最佳解决方案吗?不,正如我们在评估可维护性指数时将看到的。
大多数人会像我一样认为,圈复杂度最低的实现是最可维护的——还能有其他方式吗?
在我们的场景中,圈复杂度最低的实现并不是最可维护的。实际上在我们的场景中,情况恰恰相反。最复杂的实现是最可维护的!令人震惊!
如果你还记得,可维护性指数分数越高越好。直接说结论,多态性具有最佳的可维护性指数分数——但它也具有最高的圈复杂度。这是怎么回事?这似乎不对。
为什么最复杂的实现是最可维护的?要回答这个问题,我们必须理解可维护性指数。
可维护性指数由4个指标组成:圈复杂度、代码行数、注释数量和Halstead体积。前三个指标相对众所周知,但最后一个,Halstead体积,相对未知。像圈复杂度一样,Halstead体积试图客观地衡量代码复杂性。
简单来说,Halstead体积衡量代码中活动部件(变量、系统调用、算术、编码结构等)的数量。活动部件数量越多,复杂性越高。活动部件数量越少,复杂性越低。这解释了为什么多态实现在可维护性指数上得分很高;这些类几乎没有活动部件。查看Halstead体积的另一种方式是它衡量”活动部件”密度。
软件如果不是为了改变,那是什么?为了反映现实世界,我们引入了变化。我为每个实现添加了一种新颜色。
以下是修订后的结果。
圈复杂度 | 可维护性指数 | |
---|---|---|
Switch 语句 | 7 | 70 |
字典 | 3 | 73 |
多态性 | 17 | 95 |
switch语句和多态方法的圈复杂度都增加了一个单位,但有趣的是,字典没有增加。起初我对此感到困惑,但后来我意识到字典将颜色视为数据,而其他两个实现将颜色视为代码。我将直奔主题。
将注意力转向可维护性指数,只有一个,switch语句,可维护性下降了。多态性的可维护性分数提高了,但复杂性也增加了(我们更希望它下降)。如我上面提到的,这是反直觉的。
我们的比较显示,从复杂性角度来看,字典可以无限扩展。多态方法是迄今为止最可维护的,并且似乎随着更多场景的添加而增加可维护性。当添加新场景时,switch语句的复杂性增加,可维护性下降。即使在我们添加新场景之前,它就已经具有最差的圈复杂度和可维护性指数度量。
来自Google的Jem Finch分享了他对switch语句缺点的看法:
1. 多态方法实现在词法上彼此隔离。可以添加、删除、修改变量等,而不会有影响switch语句另一个分支中不相关代码的风险。
2. 多态方法实现保证返回到正确的位置,假设它们终止。在像C/C++/Java这样的fall through语言中的Switch语句需要容易出错的”break”语句来确保它们返回到switch之后的语句而不是下一个case块。
3. 多态方法实现的存在可以由编译器强制执行,如果缺少多态方法实现,编译器将拒绝编译程序。Switch语句不提供这样的穷尽性检查。
4. 多态方法分派是可扩展的,无需访问(或重新编译)其他源代码。向switch语句添加另一个case需要访问原始分派代码,不仅在一个地方,而且在相关枚举被switch的每个地方。
5. … 你可以独立于切换装置测试多态方法。大多数像作者给出的示例那样进行switch的函数将包含其他无法单独测试的代码;另一方面,虚拟方法调用可以。
6. 多态方法调用保证常数时间分派。不需要足够智能的编译器将本质上是线性时间结构(带有fall through的switch语句)转换为常数时间结构。
不幸的是,或者幸运的是,取决于你的阵营,大多数语言都有switch语句,它们不会很快消失。考虑到这一点,了解编译switch语句时底层发生的事情是很好的。
可能发生三种switch语句优化:
- If-elseif语句 – 当switch语句有少量case或稀疏case(非增量值,如10、250、1000)时,它被转换为if-elseif语句。
- 跳转表 – 在较大的相邻case集合(1、2、3、4、5)中,编译器将switch语句转换为跳转表。跳转表本质上是一个带有指向内存中函数的指针(想想goto语句)的哈希表。
- 二分搜索 – 对于大量稀疏case集合,编译器可以实现二分搜索来快速识别case,类似于数据库中索引的工作方式。在稀疏和相邻case数量都很大的特殊情况下,编译器将使用三种优化的组合。
总结
在面向对象的世界中,1952年构思的switch语句是软件工程师的支柱。一个值得注意的例外是Smalltalk,设计者选择排除switch语句。
与等效的替代实现(字典和多态性)相比,switch语句表现不佳。
Switch语句将继续存在,但正如我们的比较所显示的,有比switch语句更好的替代方案。
这些实现可在Github上获得。
作者:Chuck Conway 专注于软件工程和生成式人工智能。在社交媒体上与他联系:X (@chuckconway) 或访问他的 YouTube。