写无懈可击的代码
最后更新于:2022-04-01 05:22:27
在之前一节里,我提到了自己写的代码里面很少出现只有一个分支的if语句。我写出的if语句,大部分都有两个分支,所以我的代码很多看起来是这个样子:
~~~
if (...) {
if (...) {
...
return false;
} else {
return true;
}
} else if (...) {
...
return false;
} else {
return true;
}
~~~
使用这种方式,其实是为了无懈可击的处理所有可能出现的情况,避免漏掉corner case。每个if语句都有两个分支的理由是:如果if的条件成立,你做某件事情;但是如果if的条件不成立,你应该知道要做什么另外的事情。不管你的if有没有else,你终究是逃不掉,必须得思考这个问题的。
很多人写if语句喜欢省略else的分支,因为他们觉得有些else分支的代码重复了。比如我的代码里,两个else分支都是`return true`。为了避免重复,他们省略掉那两个else分支,只在最后使用一个`return true`。这样,缺了else分支的if语句,控制流自动“掉下去”,到达最后的`return true`。他们的代码看起来像这个样子:
~~~
if (...) {
if (...) {
...
return false;
}
} else if (...) {
...
return false;
}
return true;
~~~
这种写法看似更加简洁,避免了重复,然而却很容易出现疏忽和漏洞。嵌套的if语句省略了一些else,依靠语句的“控制流”来处理else的情况,是很难正确的分析和推理的。如果你的if条件里使用了`&&`和`||`之类的逻辑运算,就更难看出是否涵盖了所有的情况。
由于疏忽而漏掉的分支,全都会自动“掉下去”,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。
另外一种省略else分支的情况是这样:
~~~
String s = "";
if (x < 5) {
s = "ok";
}
~~~
写这段代码的人,脑子里喜欢使用一种“缺省值”的做法。`s`缺省为null,如果x<5,那么把它改变(mutate)成“ok”。这种写法的缺点是,当`x<5`不成立的时候,你需要往上面看,才能知道s的值是什么。这还是你运气好的时候,因为s就在上面不远。很多人写这种代码的时候,s的初始值离判断语句有一定的距离,中间还有可能插入一些其它的逻辑和赋值操作。这样的代码,把变量改来改去的,看得人眼花,就容易出错。
现在比较一下我的写法:
~~~
String s;
if (x < 5) {
s = "ok";
} else {
s = "";
}
~~~
这种写法貌似多打了一两个字,然而它却更加清晰。这是因为我们明确的指出了`x<5`不成立的时候,s的值是什么。它就摆在那里,它是`""`(空字符串)。注意,虽然我也使用了赋值操作,然而我并没有“改变”s的值。s一开始的时候没有值,被赋值之后就再也没有变过。我的这种写法,通常被叫做更加“函数式”,因为我只赋值一次。
如果我漏写了else分支,Java编译器是不会放过我的。它会抱怨:“在某个分支,s没有被初始化。”这就强迫我清清楚楚的设定各种条件下s的值,不漏掉任何一种情况。
当然,由于这个情况比较简单,你还可以把它写成这样:
~~~
String s = x < 5 ? "ok" : "";
~~~
对于更加复杂的情况,我建议还是写成if语句为好。