如何写优雅的代码(1)——灵活使用goto和 try

               

//========================================================================
//TITLE:
//    如何写优雅的代码(1)——灵活使用goto和__try
//AUTHOR:
//    norains
//DATE:
//    Thursday  16-July-2009
//Environment:
//    WINCE5.0 + VS2005
//========================================================================

    goto是毒药?凡是能用goto的地方,肯定能用结构化方式来实现相同的目的!估计很多朋友都对这论断不会陌生,甚至可以说,太熟悉了!但能实现并不代表优雅。不信?我们接下来看看。
   
    假设我们有一个函数,需要实现如下功能:将一个驱动某些内容读取到缓存区去;又因为该缓存是全局公用的,所以我们很自然采用互斥量来进行控制。首先,我们坚持采用结构化方式实现,很可能我们的代码类似如下:

BOOL ReadDeviceBuf(){ EnterCriticalSection(&g_csBuf); //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"),  FILE_WRITE_ATTRIBUTES,  0,  NULL,  OPEN_EXISTING,  FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,  NULL); if(hDev == INVALID_HANDLE_VALUE) {  LeaveCriticalSection(&g_csBuf);  return FALSE; } //获取驱动设备的缓存大小 DWORD dwSize = 0; if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE) {  CloseHandle(hDev);  LeaveCriticalSection(&g_csBuf);  return FALSE; } //分配缓存 g_pBuf = new BYTE[dwSize]; if(g_pBuf == NULL) {  CloseHandle(hDev);  LeaveCriticalSection(&g_csBuf);  return FALSE; } //从驱动中读取数据 if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_pBuf,dwSize,NULL,NULL) == FALSE) {  delete []g_pBuf;  g_pBuf = FALSE;  CloseHandle(hDev);  LeaveCriticalSection(&g_csBuf);  return FALSE; } CloseHandle(hDev); LeaveCriticalSection(&g_csBuf); return TRUE;}

 

    没错,采用这种结构化的方式的确是解决了问题。可是,我们是不是有点别扭呢?每次出错,返回FALSE之前,都必须要清理一次资源。小函数也许还不是什么大问题,只要睁大眼睛,小心翼翼,还是能在后续的返回中正确处理资源释放的。但如果函数因为要加入某些功能越来越大,又或许是别人来维护这段代码,那能保证在返回前释放资源么?
 
 
    接下来我们使用被大家鄙弃的goto,看看会发生什么情形:

BOOL ReadDeviceBuf(){ BOOL bRes = FALSE; EnterCriticalSection(&g_csBuf); DWORD dwSize = 0; //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"),  FILE_WRITE_ATTRIBUTES,  0,  NULL,  OPEN_EXISTING,  FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,  NULL); if(hDev == INVALID_HANDLE_VALUE) {  goto EXIT; } //获取驱动设备的缓存大小 //DWORD dwSize = 0; //产生编译错误:initialization of 'dwSize' is skipped by 'goto EXIT' if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE) {  goto EXIT; } //分配缓存 g_pBuf = new BYTE[dwSize]; if(g_pBuf == NULL) {  goto EXIT; } //从驱动中读取数据 if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_pBuf,dwSize,NULL,NULL) == FALSE) {  goto EXIT; } bRes = TRUE;EXIT: if(bRes == FALSE) {  delete []g_pBuf;  g_pBuf = FALSE; } CloseHandle(hDev); LeaveCriticalSection(&g_csBuf); return bRes;}

 

    怎么样?把所有的资源释放都放到EXIT段中,每个EnterCriticalSection都能对应一个LeaveCriticalSection,是不是显得比之前的更为优雅?还能说goto为鸡肋么?
   
    不过,goto也不是尽善尽美,比如变量dwSize在goto之后就不能初始化,只能将局部变量的初始化放到第一个goto之前。按照C++的建议,变量的声明最好尽可能接近使用的地方。而放在第一个goto之前,摆明就是C作风嘛!
   
    其实如果以本特例,直接声明dwSize而不进行初始化也是可行的;但这并不代表在别的情况下也能畅通无阻,也许有的程序就依赖于初始化的值,谁知道呢?
   
   
    那有没有更为优雅的?可以解决这dwSize的位置问题的?答案自然是肯定的。不过,就必须请我们的__try出场咯:

BOOL ReadDeviceBuf(){ BOOL bRes = FALSE; EnterCriticalSection(&g_csBuf); //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"),   FILE_WRITE_ATTRIBUTES,   0,   NULL,   OPEN_EXISTING,   FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,   NULL); __try {  if(hDev == INVALID_HANDLE_VALUE)  {   __leave;  }  //获取驱动设备的缓存大小  DWORD dwSize = 0;   if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE)  {   __leave;  }  //分配缓存  g_pBuf = new BYTE[dwSize];  if(g_pBuf == NULL)  {   __leave;  }  //从驱动中读取数据  if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_pBuf,dwSize,NULL,NULL) == FALSE)  {   __leave;  }  bRes = TRUE; } __finally {  if(bRes == FALSE)  {   delete []g_pBuf;   g_pBuf = FALSE;  }  CloseHandle(hDev);  LeaveCriticalSection(&g_csBuf); } return bRes;}

 

    哦耶!现在dwSize终于在它该出现的位置上了,是不是显得比goto更为优雅呢?
   
    这段改写的代码采用的是SEH机制。因为SEH机制如果需要详细解释,就不是一言两语的事情,所以在此就略过,感兴趣的朋友可以自己在网上查找资料。在这里,只是说明一点,采用SEH机制,无论如何,最后基本上一定要运行__finally段代码,除非中间有中断。
   
   
    最后一段是不是意味着凡是可以运用goto的地方都能采用__try替代?答案是否定的。特别是代码中采用了STL,SEH机制将会无能为力。
   
    不信?我们添加一点代码段来看看事情的真相。假设我们不是通过数组来保留缓存,而是保留于STL的vector中,并且成功读取之后,我们还想输出每个数值,那么我们代码可以如下:

BOOL ReadDeviceBuf(){ BOOL bRes = FALSE; EnterCriticalSection(&g_csBuf); //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"),   FILE_WRITE_ATTRIBUTES,   0,   NULL,   OPEN_EXISTING,   FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,   NULL); __try {  if(hDev == INVALID_HANDLE_VALUE)  {   __leave;  }  //获取驱动设备的缓存大小  DWORD dwSize = 0;   if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE)  {   __leave;  }  //分配缓存  g_vtBuf.resize(dwSize);  //从驱动中读取数据  if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_vtBuf[0],g_vtBuf.size(),NULL,NULL) == FALSE)  {   __leave;  }  //打印每个数据  //这里无法编译通过,提示“error C2712: Cannot use __try in functions that require object unwinding”  for(std::vector<BYTE>::iterator iter = g_vtBuf.begin(); iter != g_vtBuf.end(); iter ++)  {   printf("%d/n",*iter);  }  bRes = TRUE; } __finally {  if(bRes == FALSE)  {   g_vtBuf.clear();  }  CloseHandle(hDev);  LeaveCriticalSection(&g_csBuf); } return bRes;}

 


    很遗憾,这段代码无法编译通过。因为STL的迭代器中用到了对象,而对象会释放C++异常,而这和SEH有冲突。当然,我们完全可以用new来替代,以避开这个问题,但这样一来,却是使代码更峥嵘,离优雅更是八辈子打不到一个杆子上。
   
    这时候,还是只能用goto:

BOOL ReadDeviceBuf(){ BOOL bRes = FALSE; EnterCriticalSection(&g_csBuf); //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"),   FILE_WRITE_ATTRIBUTES,   0,   NULL,   OPEN_EXISTING,   FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,   NULL); if(hDev == INVALID_HANDLE_VALUE) {  goto EXIT; } //获取驱动设备的缓存大小 DWORD dwSize = 0;  if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE) {  goto EXIT; } //分配缓存 g_vtBuf.resize(dwSize); //从驱动中读取数据 if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_vtBuf[0],g_vtBuf.size(),NULL,NULL) == FALSE) {  goto EXIT; } //打印每个数据 //这里无法编译通过,提示“error C2712: Cannot use __try in functions that require object unwinding” for(std::vector<BYTE>::iterator iter = g_vtBuf.begin(); iter != g_vtBuf.end(); iter ++) {  printf("%d/n",*iter); } bRes = TRUE;EXIT: if(bRes == FALSE) {  g_vtBuf.clear(); } CloseHandle(hDev); LeaveCriticalSection(&g_csBuf); return bRes;}

 

    最后这个例子,从另一个角度说明了,goto并不一定是鸡肋,在某些特定的环境下,只有它才能拯救代码于优雅之境地。
   
   
    文章末尾,我们稍微总结一下。为了达到代码优雅的目的,我们首选__try;只有代码中用到了C++异常,导致和SEH冲突,我们才拿起饱受非议的goto,以完成我们优雅之目的。

           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
执行批处理程序中的条件处理。 IF [NOT] ERRORLEVEL number command IF [NOT] string1==string2 command IF [NOT] EXIST filename command NOT 指定只有条件为 false 的情况下, Windows XP 才 应该执行该命令。 ERRORLEVEL number 如果最后运行的程序返回一个等于或大于 指定数字的退出编码,指定条件为 true。 string1==string2 如果指定的文字字符串匹配,指定条件为 true。 EXIST filename 如果指定的文件名存在,指定条件为 true。 command 如果符合条件,指定要执行的命令。如果指定的 条件为 FALSE,命令后可跟一个执行 ELSE 关键字后的命令的 ELSE 命令。 ELSE 子句必须在 IF 之后出现在同一行上。例如: IF EXIST filename. ( del filename. ) ELSE ( echo filename. missing. ) 因为 del 命令需要用一个新行终止,以下子句不会有效: IF EXIST filename. del filename. ELSE echo filename. missing 由于 ELSE 命令必须与 IF 命令的尾端在同一行上,以下子句也 不会有效: IF EXIST filename. del filename. ELSE echo filename. missing 如果都放在同一行上,以下子句有效: IF EXIST filename. (del filename.) ELSE echo filename. missing 如果命令扩展名被启用,IF 会如下改变: IF [/I] string1 compare-op string2 command IF CMDEXTVERSION number command IF DEFINED variable command 其中,比较运算符可以是: EQU - 等于 NEQ - 不等于 LSS - 小于 LEQ - 小于或等于 GTR - 大于 GEQ - 大于或等于 及 /I 开关;如果该开关被指定,则说明要进行的字符串比较不分 大小。/I 开关可以用于 IF 的 string1==string2 的形式上。这些 比较都是通用的;原因是,如果 string1 和 string2 都是由数字 组成的,字符串会被转换成数字,进行数字比较。 CMDEXTVERSION 条件的作用跟 ERRORLEVEL 的一样,除了它 是在跟与命令扩展名有关联的内部版本号比较。第一个版本 是 1。每次对命令扩展名有相当大的增强时,版本号会增加一个。 命令扩展名被停用时,CMDEXTVERSION 条件不是真的。 如果已定义环境变量,DEFINED 条件的作用跟 EXISTS 的一样, 除了它取得一个环境变量,返回的结果是 true。 如果没有名为 ERRORLEVEL 的环境变量,%ERRORLEVEL% 会扩充为 ERROLEVEL 当前数值的字符串表达式;否则,您会得到 其数值。运行程序后,以下语句说明 ERRORLEVEL 的用法: goto answer%ERRORLEVEL% :answer0 echo Program had return code 0 :answer1 echo Program had return code 1 您也可以使用以上的数字比较: 如果没有名为 CMDCMDLINE 的环境变量,%CMDCMDLINE% 将在 CMD.EXE 进行任何处理前扩充为传递给 CMD.EXE 的原始 命令行;否则,您会得到其数值。 如果没有名为 CMDEXTVERSION 的环境变量, %CMDEXTVERSION% 会扩充为 CMDEXTVERSION 当前数值的 字串符表达式;否则,您会得到其数值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值