编写可读代码的艺术 (8)

第八章 拆分超长的表达式

一行代码中含有的表达式越长,它就越难以理解。注意把超长的表达式拆分成更容易理解的小块。

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


3、使用德摩根定理

如果学过电路课或是逻辑课, 应该多少会对德摩根定理有点印象。对于一个布尔表达式,有两种等价的写法。

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
}
这段代码是不是简单的多了?











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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值