不知道从什么时候开始,switch-case语句成了代码坏味道的代名词,写代码的时候小心翼翼地避开它,看到别人代码中的switch-case就皱眉头,想想其实大可不必这样,switch-case语句并不是代码坏味道的根源,坏味道来自糟糕的代码(结构)设计,比如过多的switch-case分支,或者多重switch-case嵌套等等,这些都将导致代码可读性下降,如果再加上代码风格较差,代码不对齐,那么坏味道就相当地大了。
简短的switch-case还是继续用吧,但是对于分支太多的长switch-case最好能想办法化解开,那么什么算长什么算短呢?我也不知道,就以在最低分辨率的显示器上能够在一个窗口中完整显示整个switch-case块为判断依据吧。化解长switch-case的方法有很多种,用函数封装或者宏取代case块是治标不治本的方法,使用表驱动通常是治疗这种顽症的有效方法,本文将介绍如何用表驱动方法化解长switch-case。
还是用例子说明问题吧,假设我们要为一个系统编写驱动,系统已经定义好了如下所示的复用接口(MUX):
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
用户层程序通过复用接口调用驱动,功能号就是function_no,驱动程序负责实现具体的DriverIoControl()函数完成相应的功能。这是使用switch-case的典型场景,先看一个使用switch-case的方案:
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
STATUS rc;
switch(function_no)
{
case PROCESSA:
rc = ProcessA(para_in,para_out);
break;
case PROCESSB:
rc = ProcessB(para_in,para_out);
break;
case PROCESSC:
rc = ProcessC(para_in,para_out);
break;
..........
default:
rc = UN_SUPPORT;
break
}
return rc;
}
STATUS ProcessA(PVOID para_in, PVOID para_out)
{
//一些代码....
}
STATUS ProcessB(PVOID para_in, PVOID para_out)
{
//一些代码....
}
STATUS ProcessC(PVOID para_in, PVOID para_out)
{
//一些代码....
}
................
这个方案中规中矩,但是如果驱动很复杂,功能很多,那么DriverIoControl函数代码的长度是相当可观的,好像已经闻到坏味道了,呵呵。现在换成使用宏的解决方案:
#define DISPATCH_BEGIN(func) switch(func) \
{
#define DISPATCH_FUNCTION(func_c, function) case func_c: \
rc = function(para_in,para_out); \
break;
#define DISPATCH_END(code) default: \
rc = code; \
}
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
STATUS rc;
DISPATCH_BEGIN(function_no)
DISPATCH_FUNCTION(PROCESSA,ProcessA)
DISPATCH_FUNCTION(PROCESSB,ProcessB)
DISPATCH_FUNCTION(PROCESSC,ProcessC)
........................
DISPATCH_END(UN_SUPPORT)
return rc;
}
嗯,好一点,但好不到哪里去,只是用一行代替多行而已,并不能改变代码随着功能增多线性增长的趋势。罗嗦一下,我不喜欢宏的原因很简单,目前很少有(说实话,是我确实没有见过)调试器支持对宏的展开调试。这很麻烦,当一段掺杂着宏的代码没有达到预期的目的时,你不得不一遍一遍地在心里展开你的宏,以确定它是没有问题的(或者,你根本不能确定,只能假设它没有问题)。
现在看看表驱动方案,如果系统约定的function_no是个连续的线性数字,或者是某个区间上的连续线性数字,那么驱动表可以简单就是一个线性数组,看看例子:
typedef STATUS (*ProcessFuncPtr)(PVOID para_in, PVOID para_out);
typedef struct tagDispatchItem
{
ProcessFuncPtr func_ptr;
}DISPATCH_ITEM;
DISPATCH_ITEM dispatch_table[MAX_DISPATCH_ITEM];
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
/*或者需要对function_no调整,使其区间落到[0 - MAX_DISPATCH_ITEM)*/
if((function_no >= 0) && (function_no < MAX_DISPATCH_ITEM))
return dispatch_table[function_no].func_ptr(para_in,para_out);
else
return UN_SUPPORT;
}
如果function_no不是线性的,那就需要查表:
typedef struct tagDispatchItem
{
UNIT func_no;
ProcessFuncPtr func_ptr;
}DISPATCH_ITEM;
DISPATCH_ITEM dispatch_table[MAX_DISPATCH_ITEM];
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
int i,find = -1;
for(i = 0; i < MAX_DISPATCH_ITEM; i++)
{
if(function_no == dispatch_table[i].func_no)
{
return dispatch_table[i].func_ptr(para_in,para_out);
}
}
return UN_SUPPORT;
}
使用表驱动的好处就是DriverIoControl的代码就这几行,添加新功能,只需要维护驱动表dispatch_table就行了,就这样摆脱了冗长乏味的switch-case。
前面例子中的switch-case语句中各个case分支参数比较简单整齐,也就是各个case分支都是用相同的参数para_in和para_out,如果各个分支使用的参数不整齐怎么办?那就需要封装,通常是用struct和union结合定义一个统一的数据结构做为接口参数,不同的分支dispatch函数内部根据需要从这个统一的数据结构中提取相应的数据。具体方法就是首先定义统一的dispatch函数接口,例如:
typedef STATUS (*ProcessFuncPtr)(DISPATCH_DATA *para);
然后用struct和union相结合定义DISPATCH_DATA,DISPATCH_DATA通常有这样的结构:
typedef struct tagDISPATCH_DATA
{
//此处声明所有的分支公用的数据
union
{
//此处声明processA分支使用的数据
}processA;
union
{
//此处声明processA分支使用的数据
}processB;
union
{
//此处声明processA分支使用的数据
}processC;
......
}DISPATCH_DATA;
做过Windows驱动程序的朋友肯定对IRP不陌生,IRP就是一个典型的例子。
就这样吧,打完,收工。
简短的switch-case还是继续用吧,但是对于分支太多的长switch-case最好能想办法化解开,那么什么算长什么算短呢?我也不知道,就以在最低分辨率的显示器上能够在一个窗口中完整显示整个switch-case块为判断依据吧。化解长switch-case的方法有很多种,用函数封装或者宏取代case块是治标不治本的方法,使用表驱动通常是治疗这种顽症的有效方法,本文将介绍如何用表驱动方法化解长switch-case。
还是用例子说明问题吧,假设我们要为一个系统编写驱动,系统已经定义好了如下所示的复用接口(MUX):
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
用户层程序通过复用接口调用驱动,功能号就是function_no,驱动程序负责实现具体的DriverIoControl()函数完成相应的功能。这是使用switch-case的典型场景,先看一个使用switch-case的方案:
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
STATUS rc;
switch(function_no)
{
case PROCESSA:
rc = ProcessA(para_in,para_out);
break;
case PROCESSB:
rc = ProcessB(para_in,para_out);
break;
case PROCESSC:
rc = ProcessC(para_in,para_out);
break;
..........
default:
rc = UN_SUPPORT;
break
}
return rc;
}
STATUS ProcessA(PVOID para_in, PVOID para_out)
{
//一些代码....
}
STATUS ProcessB(PVOID para_in, PVOID para_out)
{
//一些代码....
}
STATUS ProcessC(PVOID para_in, PVOID para_out)
{
//一些代码....
}
................
这个方案中规中矩,但是如果驱动很复杂,功能很多,那么DriverIoControl函数代码的长度是相当可观的,好像已经闻到坏味道了,呵呵。现在换成使用宏的解决方案:
#define DISPATCH_BEGIN(func) switch(func) \
{
#define DISPATCH_FUNCTION(func_c, function) case func_c: \
rc = function(para_in,para_out); \
break;
#define DISPATCH_END(code) default: \
rc = code; \
}
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
STATUS rc;
DISPATCH_BEGIN(function_no)
DISPATCH_FUNCTION(PROCESSA,ProcessA)
DISPATCH_FUNCTION(PROCESSB,ProcessB)
DISPATCH_FUNCTION(PROCESSC,ProcessC)
........................
DISPATCH_END(UN_SUPPORT)
return rc;
}
嗯,好一点,但好不到哪里去,只是用一行代替多行而已,并不能改变代码随着功能增多线性增长的趋势。罗嗦一下,我不喜欢宏的原因很简单,目前很少有(说实话,是我确实没有见过)调试器支持对宏的展开调试。这很麻烦,当一段掺杂着宏的代码没有达到预期的目的时,你不得不一遍一遍地在心里展开你的宏,以确定它是没有问题的(或者,你根本不能确定,只能假设它没有问题)。
现在看看表驱动方案,如果系统约定的function_no是个连续的线性数字,或者是某个区间上的连续线性数字,那么驱动表可以简单就是一个线性数组,看看例子:
typedef STATUS (*ProcessFuncPtr)(PVOID para_in, PVOID para_out);
typedef struct tagDispatchItem
{
ProcessFuncPtr func_ptr;
}DISPATCH_ITEM;
DISPATCH_ITEM dispatch_table[MAX_DISPATCH_ITEM];
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
/*或者需要对function_no调整,使其区间落到[0 - MAX_DISPATCH_ITEM)*/
if((function_no >= 0) && (function_no < MAX_DISPATCH_ITEM))
return dispatch_table[function_no].func_ptr(para_in,para_out);
else
return UN_SUPPORT;
}
如果function_no不是线性的,那就需要查表:
typedef struct tagDispatchItem
{
UNIT func_no;
ProcessFuncPtr func_ptr;
}DISPATCH_ITEM;
DISPATCH_ITEM dispatch_table[MAX_DISPATCH_ITEM];
STATUS DriverIoControl(UINT function_no, PVOID para_in, PVOID para_out)
{
int i,find = -1;
for(i = 0; i < MAX_DISPATCH_ITEM; i++)
{
if(function_no == dispatch_table[i].func_no)
{
return dispatch_table[i].func_ptr(para_in,para_out);
}
}
return UN_SUPPORT;
}
使用表驱动的好处就是DriverIoControl的代码就这几行,添加新功能,只需要维护驱动表dispatch_table就行了,就这样摆脱了冗长乏味的switch-case。
前面例子中的switch-case语句中各个case分支参数比较简单整齐,也就是各个case分支都是用相同的参数para_in和para_out,如果各个分支使用的参数不整齐怎么办?那就需要封装,通常是用struct和union结合定义一个统一的数据结构做为接口参数,不同的分支dispatch函数内部根据需要从这个统一的数据结构中提取相应的数据。具体方法就是首先定义统一的dispatch函数接口,例如:
typedef STATUS (*ProcessFuncPtr)(DISPATCH_DATA *para);
然后用struct和union相结合定义DISPATCH_DATA,DISPATCH_DATA通常有这样的结构:
typedef struct tagDISPATCH_DATA
{
//此处声明所有的分支公用的数据
union
{
//此处声明processA分支使用的数据
}processA;
union
{
//此处声明processA分支使用的数据
}processB;
union
{
//此处声明processA分支使用的数据
}processC;
......
}DISPATCH_DATA;
做过Windows驱动程序的朋友肯定对IRP不陌生,IRP就是一个典型的例子。
就这样吧,打完,收工。