编写可读代码的艺术(第二部分 简化循环和逻辑)

把控制流变得易读

条件语句中参数的顺序

关键思想:把条件、循环以及其它对控制流的改变做得越自然越好,运用一种方式使读者不用停下来读你的代码。

对比

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++的宏,和上一点有类似功能。

变量和可读性

三个问题:

  1. 变量越多,就越难全部跟踪它们的动向。
  2. 变量的作用域越大,就需要跟踪它的动向越久。
  3. 变量改变得越频繁,就越难以跟踪它的当前值。

减少变量

上一章讲了如何引入“解释”和“总结”变量来使代码更可读。这些变量很有帮助是因为它们把巨大的表达式分开。并且可以作为某种形式的文档。

在本节中,我们感兴趣的是减少不能改进可读性的变量。当移除这种变量后,新代码会更精练而且同样容易理解。

没有价值的临时变量

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;
    }
}

缩小变量的作用域

我们都听过“避免全局变量”这条建议。这是一条好的建议,因为:

  1. 很难跟踪这些全局变量在哪里以及如何使用它们。
  2. 并且通过“命名空间污染’(名字太多容易与局部变量冲突)代码可能会意外地改变全局变量的值。

关键思想:让你的变量对尽量少的代码可见,即缩小作用域。

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).

操纵一个变量的地方越多,越难确定它的当前值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值