switch-case语句是我们编码过程中常用的一种分支语句。然而正所谓成也萧何败萧何,每当我们向一个已经拥有了成百上千行的switch-case代码段中添加新的case分支的时候,我们是否有过为代码的可读性和可维护性不断下降而头疼烦恼呢。
事实上,我们可以有很多方法来避免出现这种分支有多又长的switch-case代码段,从而写出更优美的代码。在.Net中我们可以非常简单地分解switch-case中的代码。
下面选择了一个比较常见的例子:模块采用switch-case来处理接收到的Command
假设我们现在定义了以下10个CommandID。
1 /// <summary> 2 /// Definition of commands. 3 /// </summary> 4 enum CommandID 5 { 6 Abs = 1, 7 Sin = 2, 8 Sinh = 3, 9 Asin = 4, 10 Tan = 5, 11 Tanh = 6, 12 Atan = 7, 13 Cos = 8, 14 Cosh = 9, 15 Acos = 10 16 }
下面我们定义了CommandHandler1类来处理这些命令。该类采用switch-case语句分别处理不同的CommandID。我们可以将每个CommandID的处理逻辑封装在各个函数中(这里偷懒,假设Math里面定义的几个方法就是我们封装的处理逻辑),然后在每个case中调用相应的函数即可。
1 class CommandHandler1 2 { 3 /// <summary> 4 /// Handle the command. 5 /// </summary> 6 /// <param name="cmdID">The command ID of the command to be handled.</param> 7 /// <param name="cmdArg">The command argument of the command to be handled.</param> 8 /// <returns>The handle result.</returns> 9 public double HandleCommand(CommandID cmdID, double cmdArg) 10 { 11 double retValue; 12 switch (cmdID) 13 { 14 case CommandID.Abs: 15 retValue = Math.Abs(cmdArg); 16 break; 17 case CommandID.Sin: 18 retValue = Math.Sin(cmdArg); 19 break; 20 case CommandID.Sinh: 21 retValue = Math.Sinh(cmdArg); 22 break; 23 case CommandID.Asin: 24 retValue = Math.Asin(cmdArg); 25 break; 26 case CommandID.Tan: 27 retValue = Math.Tan(cmdArg); 28 break; 29 case CommandID.Tanh: 30 retValue = Math.Tanh(cmdArg); 31 break; 32 case CommandID.Atan: 33 retValue = Math.Atan(cmdArg); 34 break; 35 case CommandID.Cos: 36 retValue = Math.Cos(cmdArg); 37 break; 38 case CommandID.Cosh: 39 retValue = Math.Cosh(cmdArg); 40 break; 41 case CommandID.Acos: 42 retValue = Math.Acos(cmdArg); 43 break; 44 default: 45 retValue = this.HandleDefaultCommand(cmdArg); 46 break; 47 } 48 49 return retValue; 50 } 51 52 /// <summary> 53 /// Handle the default command. 54 /// </summary> 55 /// <param name="cmdArg">The command argument of the default command.</param> 56 /// <returns>The handle result.</returns> 57 private double HandleDefaultCommand(double cmdArg) 58 { 59 return 0; 60 } 61 }
在CommandHandler1中,我们如果新增了一个命令,那么就需要增加一个处理新命令的方法,同时修改HandleCommand方法体,在其中添加一个case分支并调用新增的方法。
下面利用字典和委托CommandHandler1里面的switch-case代码段。我们新定义了一个类CommandHandler2,将处理每个CommandID的委托方法保存在一个字典表中(cmdHandlers),在HandleCommand方法体中,通过cmdID找到对应的委托方法来处理响应的cmdID。
1 class CommandHandler2 2 { 3 /// <summary> 4 /// The dictionary contains all the command handlers to handle the commands. 5 /// </summary> 6 private Dictionary<CommandID, Func<double, double>> cmdHandlers = new Dictionary<CommandID, Func<double, double>> 7 { 8 {CommandID.Abs, Math.Abs}, {CommandID.Sin, Math.Sin}, {CommandID.Sinh, Math.Sinh}, {CommandID.Asin, Math.Asin}, 9 {CommandID.Tan, Math.Tan}, {CommandID.Tanh, Math.Tanh}, {CommandID.Atan, Math.Atan}, {CommandID.Cos, Math.Cos}, 10 {CommandID.Cosh, Math.Cosh}, {CommandID.Acos, Math.Acos} 11 }; 12 13 /// <summary> 14 /// Handle the command. 15 /// </summary> 16 /// <param name="cmdID">The command ID of the command to be handled.</param> 17 /// <param name="cmdArg">The command argument of the command to be handled.</param> 18 /// <returns>The handle result.</returns> 19 public double HandleCommand(CommandID cmdID, double cmdArg) 20 { 21 var cmdHandler = this.cmdHandlers.ContainsKey(cmdID) ? this.cmdHandlers[cmdID] : this.HandleDefaultCommand; 22 return cmdHandler(cmdArg); 23 } 24 25 /// <summary> 26 /// Handle the default command. 27 /// </summary> 28 /// <param name="cmdArg">The command argument of the default command.</param> 29 /// <returns>The handle result.</returns> 30 private double HandleDefaultCommand(double cmdArg) 31 { 32 return 0; 33 } 34 }
当我们新增一个命令时,只需要增加一个处理新命令的方法,同时将这个新命令及其对应的委托方法添加到字典表中即可。
下面我们来看一下这两种方法的性能。在测试性能时,我们将所有的cmd处理方法全都替换成了HandleDefaultCommand。
1 class CommandHandlerTest1 2 { 3 /// <summary> 4 /// Handle the command. 5 /// </summary> 6 /// <param name="cmdID">The command ID of the command to be handled.</param> 7 /// <param name="cmdArg">The command argument of the command to be handled.</param> 8 /// <returns>The handle result.</returns> 9 public double HandleCommand(CommandID cmdID, double cmdArg) 10 { 11 double retValue; 12 switch (cmdID) 13 { 14 case CommandID.Abs: 15 retValue = this.HandleDefaultCommand(cmdArg); 16 //retValue = Math.Abs(cmdArg); 17 break; 18 case CommandID.Sin: 19 retValue = this.HandleDefaultCommand(cmdArg); 20 //retValue = Math.Sin(cmdArg); 21 break; 22 case CommandID.Sinh: 23 retValue = this.HandleDefaultCommand(cmdArg); 24 //retValue = Math.Sinh(cmdArg); 25 break; 26 case CommandID.Asin: 27 retValue = this.HandleDefaultCommand(cmdArg); 28 //retValue = Math.Asin(cmdArg); 29 break; 30 case CommandID.Tan: 31 retValue = this.HandleDefaultCommand(cmdArg); 32 //retValue = Math.Tan(cmdArg); 33 break; 34 case CommandID.Tanh: 35 retValue = this.HandleDefaultCommand(cmdArg); 36 //retValue = Math.Tanh(cmdArg); 37 break; 38 case CommandID.Atan: 39 retValue = this.HandleDefaultCommand(cmdArg); 40 //retValue = Math.Atan(cmdArg); 41 break; 42 case CommandID.Cos: 43 retValue = this.HandleDefaultCommand(cmdArg); 44 //retValue = Math.Cos(cmdArg); 45 break; 46 case CommandID.Cosh: 47 retValue = this.HandleDefaultCommand(cmdArg); 48 //retValue = Math.Cosh(cmdArg); 49 break; 50 case CommandID.Acos: 51 retValue = this.HandleDefaultCommand(cmdArg); 52 //retValue = Math.Acos(cmdArg); 53 break; 54 default: 55 retValue = this.HandleDefaultCommand(cmdArg); 56 break; 57 } 58 59 return retValue; 60 } 61 62 /// <summary> 63 /// Handle the default command. 64 /// </summary> 65 /// <param name="cmdArg">The command argument of the default command.</param> 66 /// <returns>The handle result.</returns> 67 private double HandleDefaultCommand(double cmdArg) 68 { 69 return 0; 70 } 71 } 72 73 class CommandHandlerTest2 74 { 75 /// <summary> 76 /// The dictionary contains all the command handlers to handle the commands. 77 /// </summary> 78 //private Dictionary<CommandID, Func<double, double>> cmdHandlers = new Dictionary<CommandID, Func<double, double>> 79 //{ 80 // {CommandID.Abs, Math.Abs}, {CommandID.Sin, Math.Sin}, {CommandID.Sinh, Math.Sinh}, {CommandID.Asin, Math.Asin}, 81 // {CommandID.Tan, Math.Tan}, {CommandID.Tanh, Math.Tanh}, {CommandID.Atan, Math.Atan}, {CommandID.Cos, Math.Cos}, 82 // {CommandID.Cosh, Math.Cosh}, {CommandID.Acos, Math.Acos} 83 //}; 84 private Dictionary<CommandID, Func<double, double>> cmdHandlers; 85 86 public CommandHandlerTest2() 87 { 88 cmdHandlers = new Dictionary<CommandID, Func<double, double>> 89 { 90 {CommandID.Abs, this.HandleDefaultCommand}, {CommandID.Sin, this.HandleDefaultCommand}, 91 {CommandID.Sinh, this.HandleDefaultCommand}, {CommandID.Asin, this.HandleDefaultCommand}, 92 {CommandID.Tan, this.HandleDefaultCommand}, {CommandID.Tanh, this.HandleDefaultCommand}, 93 {CommandID.Atan, this.HandleDefaultCommand}, {CommandID.Cos, this.HandleDefaultCommand}, 94 {CommandID.Cosh, this.HandleDefaultCommand}, {CommandID.Acos, this.HandleDefaultCommand} 95 }; 96 } 97 98 /// <summary> 99 /// Handle the command. 100 /// </summary> 101 /// <param name="cmdID">The command ID of the command to be handled.</param> 102 /// <param name="cmdArg">The command argument of the command to be handled.</param> 103 /// <returns>The handle result.</returns> 104 public double HandleCommand(CommandID cmdID, double cmdArg) 105 { 106 var cmdHandler = this.cmdHandlers.ContainsKey(cmdID) ? this.cmdHandlers[cmdID] : this.HandleDefaultCommand; 107 return cmdHandler(cmdArg); 108 } 109 110 /// <summary> 111 /// Handle the default command. 112 /// </summary> 113 /// <param name="cmdArg">The command argument of the default command.</param> 114 /// <returns>The handle result.</returns> 115 private double HandleDefaultCommand(double cmdArg) 116 { 117 return 0; 118 } 119 } 120 121 class Program 122 { 123 static void Main(string[] args) 124 { 125 List<CommandID> cmdList = new List<CommandID>() 126 { 127 CommandID.Abs, CommandID.Sin, CommandID.Sinh, CommandID.Asin, CommandID.Tan, 128 CommandID.Tanh, CommandID.Atan, CommandID.Cos, CommandID.Cosh, CommandID.Acos 129 }; 130 131 Stopwatch watch = new Stopwatch(); 132 133 watch.Start(); 134 CommandHandlerTest1 test1 = new CommandHandlerTest1(); 135 for (int i = 0; i < 1000000; i++) 136 { 137 for (int j = 0; j < 10; j++) 138 { 139 test1.HandleCommand(cmdList[j], 0.1); 140 } 141 } 142 143 watch.Stop(); 144 Console.WriteLine(watch.ElapsedMilliseconds); 145 146 watch.Reset(); 147 watch.Start(); 148 CommandHandlerTest2 test2 = new CommandHandlerTest2(); 149 for (int i = 0; i < 1000000; i++) 150 { 151 for (int j = 0; j < 10; j++) 152 { 153 test2.HandleCommand(cmdList[j], 0.1); 154 } 155 } 156 157 watch.Stop(); 158 Console.WriteLine(watch.ElapsedMilliseconds); 159 160 Console.ReadLine(); 161 } 162 }
原本认为采用字典表+委托的方法性能应该比switch-case高,但测试结果却令人失望(CommandHandler1比CommandHandler2反而高了近50%)。分析这里面可能的原因:
1. 通过委托调度处理方法比直接调用方法效率相对较低;
2. 编译器对switch-case的代码进行了一定的优化。
好在一般对这里的性能要求不是很高,100W次平均下来相差零点几微秒,而在又多又长的switch-case代码段中,一般是可以接受的。而对于比较简短的switch-case代码段,也就没有必要采用第二种方式去替换了。
当然这里所举的处理Command的例子,有更好的解决方案。由于这篇文章只是讲述如何用一种更优美的方式来替代switch-case,因此就不再详细描述了。