第八章 拆分超长的表达式
一行代码中含有的表达式越长,它就越难以理解。注意把超长的表达式拆分成更容易理解的小块。
1、用作解释的变量
拆分表达式最简单的一个办法就是引入一个额外的变量,让它来表示一个小一点的子表达式,姑且把这个小变量叫做“解释变量”吧,因为它可以解释子表达式的含义。
if line.split(':')[0].strip() == "root"
...
如果初次看这段代码,你能说出if前半句中的表达式是啥含义吗?如果是这样呢:
username = line.split(':')[0].strip()
if(username == "root")
...
读者一眼就能看出来,那个表达式是在求用户名字,这样是不是直观好多。
2、总结变量
有时候即使一个表达式不需要解释(它的含义一目了然),把它装入一个新变量仍然有用。姑且叫它总结变量吧,它的目的是用一个很短的名字来总结一大段代码,这个代码更容易管理和思考。
if(request.user.id == document.owner_id)
{
//user can edit this document...
}
...
if(request.user.id != document.owner_id)
{
//document is read-only...
}
if语句中的表达式看上去不长,但是它包含了很多的变量,不利于阅读。这段代码的主要意图是确认该用户是否拥有此文档,如果增加一个类似的变量会表达的更清楚。
final 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...
}
如果学过电路课或是逻辑课, 应该多少会对德摩根定理有点印象。对于一个布尔表达式,有两种等价的写法。
1)not (a or b or c) <=> (not a) and (not b) and (not c)
2) not (a and b and c) <=> (not a) or (not b) or (not c)
有时候我们写代码,可以参照这些法则让代码的可读性提高。比如,如果代码是这样的,
if(!(file_exits && !is_protected)) Error("Sorry, could not read file");
这段代码有很多的负逻辑,看起来有点烧脑,如果我们改成下面这个样子:
if(!file_exits || is_protected)) Error("Sorry, could not read file");
是不是读起来,顺畅很多。
4、滥用短路逻辑
编码时,有时候运用短路逻辑会很有用。比如(a || b),如果a为真的话,b就不会计算了;同理还有(a && b),a为假,b也不会被计算。但是如果代码总滥用这种技巧,会让代码的可读性变差。
asser((!(bucket = FindBucket(key))) || !bucket->IsOccupied());
看上面的代码,作者使用短路逻辑,将两种处理合并到了一个语句中,代价就是使用了许多的负逻辑。虽然不长,但是却会让读者停下来,思考下这句代码的含义。这种情况下,把它们分开处理是一个更好的方法。
bucket = FindBucket(key);
if(bucket != NULL)
asser(!bucket->IsOccupied());
要小心“智能”的代码,它们往往会在以后让别人读起来感到困惑。多数情况下,如果表达式中多为正逻辑的话,我们还是可以使用短路技巧达到代码整洁的作用,比如:
if(object && object->method()) ...
示例:与复杂的逻辑作斗争
假设需要实现这个Range类:
struct Range
{
int begin;
int end;
//for example, [0,5) overlaps [3,8)
bool OverlapsWith(Range other);
};
这个类中的每组范围的终端是非包含的,即前闭后开。
下面是一个实现,它检查否是自身范围内的任意一个端点在other的范围之内。
bool Range::OverlapsWith(Range other)
{
//check if 'begin' or 'end' falls inside 'other'
return (begin >= other.begin && begin <= other.end) ||
(end >= other.begin && end <= other.end);
}
尽管只有两行代码,但是这么多的表达式里,包含了不少的逻辑,看起来是不是比较烧脑。而且这段代码里,确确实实还存在一个bug。它会认为Range[0,2)与Range[2,4)重复,实际上它们并不重复。问题就在于处理多层逻辑时将<=或是<搞混淆了。修复后的代码如下:
return (begin >= other.begin && begin < other.end) ||
<span style="white-space:pre"> </span>(end > other.begin && end <= other.end);
这样是不是正确了呢?还是不对的。这段代码会忽略begin/end完全包含other的情况,比如Range[0,4)与Range[1,2),这段代码不会认为他俩重叠,实际上不是这样。既然如此,我们只好再修改下这段代码:
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end) ||
(begin <= other.begin && end >= other.end);
终于是最终的代码了。不过这样的代码是不是有点复杂了,很难保证阅读者一次就理解它的意思,我们自己也很难保证一次就写出正确的代码。
这个时候就应该从其他方面寻找解决方案了。overlap的反方向是“不重叠”,如果将两个范围一定不重叠的情况确定的话,那么其他情况下,就肯定有重叠了。而判断两个范围一定不重叠的情况就比较简单了,只存在两个:
a)、另一个范围在这个范围开始前结束;
b)、另一个范围在这个范围结束后开始。
跟着这个思路,编写的代码如下:
bool Range::OverlapsWith(Range other)
{
if(other.end <= begin) return false; //other end before we begin
if(other.begin >= end) return false; //other begin after we end
return true; //only possibility left:they overlap
}
这段代码是不是简单的多了?