把控制流变得易读
条件语句中参数的顺序
关键思想:把条件、循环以及其它对控制流的改变做得越自然越好,运用一种方式使读者不用停下来读你的代码。
对比
if(length<10){// 1
}
if(10>length){// 2
}
while(bytes_received < bytes_expected){// 3
}
while(bytes_expected > bytes_received){// 4
}
1优于2,3优于4。
按照什么原则呢?
比较符号的左侧 | 比较符号的右侧 |
---|---|
“被询问的”表达式,它的值更倾向于不断变化。 | 用来作比较的表达式,它的值更倾向于表达式 |
按照这个原则,就可以解释1 3为何优于2 4了。
“尤达表示法”已经过时了,现在编译器都会提醒。(所以找一款好的编辑器吧,事半功倍)。
if/else语句块的性质
你通常可以互换if/else各语句块的位置
// case1和case2可以互换
if (){
//case 1
}else{
//case 2
}
但在某些情况下,其中一种顺序比另一种更好。
- 首先处理正逻辑而不是负逻辑的情况,例如if(debug)而不是if(!debug)。
- 先处理掉简单的情况。这种方式可能还会使得if和else在屏幕之内都可见,这很好。
- 先处理有趣的或者是可疑的情况。
当然有时候这三种倾向性会有冲突,这就需要自己判断了。
?:条件表达式(三目运算符)
建议:默认情况下都用if/else,三目运算符?:只有在最简单的情况下使用,不然可能会让代码难以理解。
避免do/while循环
如题
从函数中提前返回
臭名昭著的goto
最小化嵌套
// 问题1:不断切换SUCCESS和non_SUCCESS的条件,容易使人混乱。
// 问题2:逻辑可能正确,但条件混在一起却很容易使人混乱。
if (user_ result== SUCCESS) {
if (permission_result != SUCCESS) {
reply.NriteErrors("error reading permissions");
reply.Done() ;
return;
}
reply.WriteErrors("");
}else {
reply.WriteErrors (user_result);
}
reply.Done();
嵌套是如何积累而成的
这段照抄,感觉分析的很到位
在我们修正前面的事例代码之前,先来看看什么导致它现在的这个样子的。一开始代码是很简单的:
if(user_result == SUCCESS){
reply.WriteErrors("");
}else{
reply.WriteErrors(user_result);
}
reply.Done();
但是后来的程序员增加了第二个操作:
if (user_ result== SUCCESS) {
if (permission_result != SUCCESS) {
reply.NriteErrors("error reading permissions");
reply.Done() ;
return;
}
reply.WriteErrors("");
...
这个改动有合理的地方——该程序员要插入一段新代码,并且她找到了最容易插入的地方。新代码很整洁,而且很明确。这个改动的差异也很清晰——这看上去对于她来讲,像是个简单的改动。
但是以后当其他人遇到这段代码时,所有的上下文早已不在了。这就是你在本节一开始读到这段代码时的情况,你不得不一下子全盘接受它。
关键思想:当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。
通过提早返回来减少嵌套
下面改进,像这种嵌套可以通过马上处理“失败情况”并从函数早返回来减少:
if (user_result != SUCCESS) {
reply.WriteErrors (user_result) ;
reply.Done();
return ;
}
if (permission_result != SUCCESS) {
reply.WriteErrors(permissio_result) ;
reply.Done( );
return ;
}
reply.WriteErrors("") ;
reply.Done();
上面这段代码只有一层嵌套,而不是两层。但更重要的是,读者不再需要从思维堆栈里“出栈”了——每个if块都以一个return结束。
减少循环内的嵌套
提早返回这个技术并不总是合适的。例如,下面代码在循环中有嵌套:
for (int i= 0; i < results.size(); i++) {
if (results[i] != NULL) {
non_null_count++;
if (results[i]->name !="") {
cout <<"Considering candidate..." << endl;
...
}
}
}
在循环中,与提早返回类似的技术是continue:
for (int i= 0; i < results.size(); i+) {
if (results[i]== NULL) continue;
non_null_count++;
if (results[i]->name == "") continue;
cout <<"Considering candidate..." << endl;
...
}
与if(…)return;在函数中所扮演的保护语句一样,这些if(…) continue;语句是循环
中的保护语句。
一般来讲,continue语句让人很困惑,因为它让读者不能连续地阅读,就像循环中有goto语句一样。但是在这种情况中,循环中的每个迭代是相互独立的(这是一种“foreach” 循环),因此读者可以很容易地领悟到这里continue的意思是“跳过该项”。
你能理解执行的流程吗
拆分超长的表达式
用做解释的变量
我们大多数人同时只能考虑3~4件“事情”。代码中的表达式越长,他就越难理解。
用作解释的变量
插入一个解释变量
// 1==>2
if line.split(' :')[o].strip()=="root":// 1
username= line.split(' :' )[0].strip()// 2
if username == "root" :
总结变量
if (request.user.id == document.owner_id) {
// user can
edit this document...
}
...
if (request.user.id != document.owner_id) {
// document is read-only...
}
这里的表达式request.user.id== document.owner.id看上去可能并不长,但它包含5个变量,所以需要多花点时间来想一想如何处理它。
这段代码中的主要概念是:“该用户拥有此文档吗?”这个概念可以通过增加一个总结
变量来表达得更清楚。
不得不说下面的写法确实更容易理解。
boolean user_owns_document = (request.user.id == document.owner_id);
if (user_owns_document) {
// user can edit this document...
}
...
if (!user_owns_document) {
// document is read-only...
}
使用德摩根定理
口诀:分别取反,转换与/或
利用口诀简化下面代码:
if (! (file_exists && !is_protected)) Error("Sorry, could not read file.");
//转化为:
if (! file_exists| |is_protected) Error("Sorry, could not read file." );
滥用短路逻辑
关键思想:要小心“智能”的小代码段,它们往往在以后会让别人读起来感到困惑。简而言之,你不要为了简化代码,而给别人设置理解上的坑。
例子: 与复杂的逻辑战斗
拆分巨大的语句
把重复使用的长字符串提取出来命名放在函数内的起始位置,这样的好处是:
- 多处使用时,避免重复打入大量信息,从而减少录入错误的情况
- 如需改动,改一处即可
- 缩短了代码的长度,使得代码更容易阅读
另一个简化表达式的创意方法
介绍了C++的宏,和上一点有类似功能。
变量和可读性
三个问题:
- 变量越多,就越难全部跟踪它们的动向。
- 变量的作用域越大,就需要跟踪它的动向越久。
- 变量改变得越频繁,就越难以跟踪它的当前值。
减少变量
上一章讲了如何引入“解释”和“总结”变量来使代码更可读。这些变量很有帮助是因为它们把巨大的表达式分开。并且可以作为某种形式的文档。
在本节中,我们感兴趣的是减少不能改进可读性的变量。当移除这种变量后,新代码会更精练而且同样容易理解。
没有价值的临时变量
now = datetime.datetime.now()
root._message.last view_ time = now
now是一个值得保留的变量吗? 不是,下面是原因:
- 它没有拆分任何复杂的表达式。
- 它没有做更多的澄清一一表达式datetime.datetime.now()已经很清楚了。
- 它只用过一次,因此它并没有压缩任何冗余代码。
没有了now,代码一样容易理解。
减少中间结果
var remove._one = function (array,value_to_remove) {
var index_to_remove = null;
for(var i= 0; i < array.length; i += 1) {
if (array[i]===value._to_remove) {
1ndex.to_ remove = i;
break;
}
}
if (index_to_remove !== null) {
array.splice(index_to_remove,1) ;
}
};
其实可以直接写成下面这样,不需要中间变量index_to_remove:
var remove_one = function (array,value_ to_remove) {
for (var i= 0; i < array.length; i +=1){
if (array[i]===value_to remove) {
array.splice(i,1);
return ;
}
}
};
减少控制流变量
boolean done =false;
while (/* condition */ && !done) {
...
if (...) {
done = true;
continue;
}
}
通过结构化编程去除控制变量:
while (/* condition */) {
...
if (...) {
break;
}
}
缩小变量的作用域
我们都听过“避免全局变量”这条建议。这是一条好的建议,因为:
- 很难跟踪这些全局变量在哪里以及如何使用它们。
- 并且通过“命名空间污染’(名字太多容易与局部变量冲突)代码可能会意外地改变全局变量的值。
关键思想:让你的变量对尽量少的代码可见,即缩小作用域。
C++中if语句的作用域
PaymentInfo* info= database.ReadPaymentInfo() ;
if (1nfo) {
cout<<"User paid:"<< info->amount() << endl;
}
//Many more lines of code below
如果info只在if条件句中使用:
if (PaymentInfo* info= database.ReadPaymentInfo() {
cout << "User paid:"<< info->amount() <<end1;
只写一次变量就好
不断变化的变量让人难以理解。跟踪这种变量的值更有难度。
“永久固定”的变量更容易思考。当前,像这种常量:
static const int NUM_THREADS = 10;
不需要读者思考很多。基于同样的原因,鼓励在C++中使用const (在Java中使
用final).
操纵一个变量的地方越多,越难确定它的当前值。