大神王垠说避免使用continue和break。他说:
出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种:
1.如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue。
2.如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。
3.有时候你可以把break替换成return,从而去掉break。
4.如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。
现在我有这样一段代码:代码要处理的就是检查name
中有没有非规范的字符。代码功能是没有问题的。
int is_valid_name(char *name, uint16_t len)
{
char regularChrs[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
char tmp[sizeof(uint16_t)]={0};
uint16_t regLen,i,j;
if(len <= 0)
return 1;
memcpy(tmp, name, len);
regLen = strlen(regularChrs);
for(i=0; i<len; i++)
{
for(j=0; j<regLen; j++)
{
if(tmp[i] != regularChrs[j])
continue;
else
break;
}
if(j == regLen)
{
return 1;
}
}
return 0;
}
如何改写呢?
这里要注意,我们的核心逻辑里面包含了contine
和break
,其控制流程如下图所示:
但仔细分析一下,我们可以发现continue
语句是无效的语句,因为即使没有它,控制流程也能返回。那么实际的逻辑语句是:
...
for(j=0; j<regLen; j++)
{
if(tmp[i] == regularChrs[j])
break;
}
...
改写步骤
现在情况简单明了,只需要将break
语句消除掉即可,方法是:如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。并将中间循环提取出来形成一个单独函数,用于判断一个字符是否是规范字符,以使表达更简洁;另外对于返回值,要根据函数语义进行修改。
const char regularChrs[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
uint16_t regLen = strlen(regularChrs);
int is_valid_letter(char ch)
{
uint16_t index = 0;
while(index < regLen && ch != regluarChrs[index])
{
index += 1;
}
//退出循环后可以判断是否遍历完regularChrs。
//如果遍历完,说明ch不等于规范字符串中的任何一个。invalid
if(j == regLen)
{
return 0; //invalid
}else
{
return 1; //valid
}
}
然后将原来代码中相关的逻辑处理用这个函数代替。
int is_valid_name(char *name, uint16_t len)
{
uint16_t i = 0;
if(len <= 0)
return 0;
while(i < len && is_valid_letter(name[i]) )
{
i += 1;
}
//遍历完整个name发现都是valid letter,即name也是valid
if(i == len)
{
return 1; //valid
}else
{
return 0; //invalid
}
}
仔细观察上面两个函数,它们代码结构是相似的,不同在于两个函数的判断标准是相反的,因此返回值正好相反,这是为了保证函数语义的一致性。
这样的两个不同的逻辑判断如果在一个函数里面实现的时候,就会因为逻辑嵌套且上层与下层的逻辑实现不同而造成背离感,这种背离感是这种复杂逻辑所固有的。在我们修改版本中体现的更好。修改版本中,如果你保证上层与下层函数语义的一致性,那么就会失去实现逻辑的一致性;如果保证实现的一致性,那么就会失去函数语义的一致性。
我们可以想象用is_invalid_letter
函数来重新实现我们的功能。代码如下:
int is_invalid_letter(char ch)
{
uint16_t index = 0;
while(index < regLen && ch != regluarChrs[index])
{
index += 1;
}
//退出循环后可以判断是否遍历完regularChrs。
//如果遍历完,说明ch不等于规范字符串中的任何一个。invalid
if(j == regLen)
{
return 1; //invalid
}else
{
return 0; //valid
}
}
int is_valid_name(char *name, uint16_t len)
{
uint16_t i = 0;
if(len <= 0)
return 0;
while(i < len && !is_invalid_letter(name[i]) )
{
i += 1;
}
//遍历完整个name发现都是valid letter,说明name也是valid
if(i == len)
{
return 1; //valid
}else
{
return 0; //invalid
}
}
此时大家会发现,两个函数实现逻辑已经是相同的了,但是函数语义已经背反。
总结
原始代码里面,看起来代码量比较少,但是语义不清,理解起来要困难很多,而且尤其是continue
和break
在一起使用时,使整个代码逻辑上跟纠缠的面条一样,增加阅读难度。经过整理之后的代码,代码行数虽然增加,但是语义清晰,容易理解,而且同构的代码在逻辑层次上也能捋清了。
大神诚不欺我矣。
再进一步,这样的一个功能,还可以优化一下,函数接口参数增加对规范字符的支持。函数原型是:
int is_valid_name(char *name, uint16_t len, char *regular, uint16_t reglen)
至于实现,相信大家!
网友评论