[关闭]
@yiltoncent 2015-12-07T08:44:10.000000Z 字数 2632 阅读 985

如何避免使用continue和break

C语言基础


大神王垠说避免使用continue和break。他说:

出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种:

1.如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue。
2.如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。
3.有时候你可以把break替换成return,从而去掉break。
4.如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。

现在我有这样一段代码:代码要处理的就是检查name中有没有非规范的字符。

  1. int is_valid_name(char *name, uint16_t len)
  2. {
  3. char regularChrs[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  4. char tmp[sizeof(uint16_t)]={0};
  5. uint16_t regLen,i,j;
  6. if(len <= 0)
  7. return 1;
  8. memcpy(tmp, name, len);
  9. regLen = strlen(regularChrs);
  10. for(i=0; i<len; i++)
  11. {
  12. for(j=0; j<regLen; j++)
  13. {
  14. if(tmp[i] != regularChrs[j])
  15. continue;
  16. else
  17. break;
  18. }
  19. if(j == regLen)
  20. {
  21. return 1;
  22. }
  23. }
  24. return 0;
  25. }

如何改写呢?

这里要注意,我们的核心逻辑里面包含了continebreak,其控制流程如下图所示:
QQ图片20151207160802.jpg-1034kB

但仔细分析一下,我们可以发现continue语句是无效的语句,因为即使没有它,控制流程也能返回。那么实际的逻辑语句是:

  1. ...
  2. for(j=0; j<regLen; j++)
  3. {
  4. if(tmp[i] == regularChrs[j])
  5. break;
  6. }
  7. ...

改写步骤

现在情况简单明了,只需要将break语句消除掉即可,方法是:如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。并将中间循环提取出来形成一个单独函数,用于判断一个字符是否是规范字符,以使表达更简洁;另外对于返回值,要根据函数语义进行修改。

  1. const char regularChrs[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  2. uint16_t regLen = strlen(regularChrs);
  3. int is_valid_letter(char ch)
  4. {
  5. uint16_t index = 0;
  6. while(index < regLen && ch != regluarChrs[index])
  7. {
  8. index += 1;
  9. }
  10. //退出循环后可以判断是否遍历完regularChrs。
  11. //如果遍历完,说明ch不等于规范字符串中的任何一个。invalid
  12. if(j == regLen)
  13. {
  14. return 0; //invalid
  15. }else
  16. {
  17. return 1; //valid
  18. }
  19. }

然后将原来代码中相关的逻辑处理用这个函数代替。

  1. int is_valid_name(char *name, uint16_t len)
  2. {
  3. uint16_t i = 0;
  4. if(len <= 0)
  5. return 0;
  6. while(i < len && is_valid_letter(name[i]) )
  7. {
  8. i += 1;
  9. }
  10. //遍历完整个name发现都是valid letter,即name也是valid
  11. if(i == len)
  12. {
  13. return 1; //valid
  14. }else
  15. {
  16. return 0; //invalid
  17. }
  18. }

仔细观察上面两个函数,它们代码结构是相似的,不同在于两个函数的判断标准是相反的,因此返回值正好相反,这是为了保证函数语义的一致性
这样的两个不同的逻辑判断如果在一个函数里面实现的时候,就会因为逻辑嵌套且上层与下层的逻辑实现不同而造成背离感,这种背离感是这种复杂逻辑所固有的。在我们修改版本中体现的更好。修改版本中,如果你保证上层与下层函数语义的一致性,那么就会失去实现逻辑的一致性;如果保证实现的一致性,那么就会失去函数语义的一致性。
我们可以想象用is_invalid_letter函数来重新实现我们的功能。代码如下:

  1. int is_invalid_letter(char ch)
  2. {
  3. uint16_t index = 0;
  4. while(index < regLen && ch != regluarChrs[index])
  5. {
  6. index += 1;
  7. }
  8. //退出循环后可以判断是否遍历完regularChrs。
  9. //如果遍历完,说明ch不等于规范字符串中的任何一个。invalid
  10. if(j == regLen)
  11. {
  12. return 1; //invalid
  13. }else
  14. {
  15. return 0; //valid
  16. }
  17. }
  18. int is_valid_name(char *name, uint16_t len)
  19. {
  20. uint16_t i = 0;
  21. if(len <= 0)
  22. return 0;
  23. while(i < len && !is_invalid_letter(name[i]) )
  24. {
  25. i += 1;
  26. }
  27. //遍历完整个name发现都是valid letter,说明name也是valid
  28. if(i == len)
  29. {
  30. return 1; //valid
  31. }else
  32. {
  33. return 0; //invalid
  34. }
  35. }

此时大家会发现,两个函数实现逻辑已经是相同的了,但是函数语义已经背反。

总结

原始代码里面,看起来代码量比较少,但是语义不清,理解起来要困难很多,而且尤其是continuebreak在一起使用时,使整个代码逻辑上跟纠缠的面条一样,增加阅读难度。经过整理之后的代码,代码行数虽然增加,但是语义清晰,容易理解,而且同构的代码在逻辑层次上也能捋清了。
大神诚不欺我矣。

再进一步,这样的一个功能,还可以优化一下,函数接口参数增加对规范字符的支持。函数原型是:

  1. int is_valid_name(char *name, uint16_t len, char *regular, uint16_t reglen)

至于实现,相信大家!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注