ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
程序语言都喜欢标新立异,提供这样那样的“特性”,然而有些特性其实并不是什么好东西。很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。很多人盲目的追求“短小”和“精悍”,或者为了显示自己头脑聪明,学得快,所以喜欢利用语言里的一些特殊构造,写出过于“聪明”,难以理解的代码。 并不是语言提供什么,你就一定要把它用上的。实际上你只需要其中很小的一部分功能,就能写出优秀的代码。我一向反对“充分利用”程序语言里的所有特性。实际上,我心目中有一套最好的构造。不管语言提供了多么“神奇”的,“新”的特性,我基本都只用经过千锤百炼,我觉得值得信奈的那一套。 现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。 * 避免使用自增减表达式(i++,++i,i--,--i)。这种自增减操作表达式其实是历史遗留的设计失误。它们含义蹊跷,非常容易弄错。它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。 其实这两个表达式完全可以分解成两步,把读和写分开:一步更新i的值,另外一步使用i的值。比如,如果你想写`foo(i++)`,你完全可以把它拆成`int t = i; i += 1; foo(t);`。如果你想写`foo(++i)`,可以拆成`i += 1; foo(i);` 拆开之后的代码,含义完全一致,却清晰很多。到底更新是在取值之前还是之后,一目了然。 有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在for循环的update部分,比如`for(int i = 0; i < 5; i++)`。另一种情况是写成单独的一行,比如`i++;`。这两种情况是完全没有歧义的。你需要避免其它的情况,比如用在复杂的表达式里面,比如`foo(i++)`,`foo(++i) + foo(i)`,…… 没有人应该知道,或者去追究这些是什么意思。 * 永远不要省略花括号。很多语言允许你在某种情况下省略掉花括号,比如C,Java都允许你在if语句里面只有一句话的时候省略掉花括号: ~~~ if (...) action1(); ~~~ 咋一看少打了两个字,多好。可是这其实经常引起奇怪的问题。比如,你后来想要加一句话`action2()`到这个if里面,于是你就把代码改成: ~~~ if (...) action1(); action2(); ~~~ 为了美观,你很小心的使用了`action1()`的缩进。咋一看它们是在一起的,所以你下意识里以为它们只会在if的条件为真的时候执行,然而`action2()`却其实在if外面,它会被无条件的执行。我把这种现象叫做“光学幻觉”(optical illusion),理论上每个程序员都应该发现这个错误,然而实际上却容易被忽视。 那么你问,谁会这么傻,我在加入`action2()`的时候加上花括号不就行了?可是从设计的角度来看,这样其实并不是合理的作法。首先,也许你以后又想把`action2()`去掉,这样你为了样式一致,又得把花括号拿掉,烦不烦啊?其次,这使得代码样式不一致,有的if有花括号,有的又没有。况且,你为什么需要记住这个规则?如果你不问三七二十一,只要是if-else语句,把花括号全都打上,就可以想都不用想了,就当C和Java没提供给你这个特殊写法。这样就可以保持完全的一致性,减少不必要的思考。 有人可能会说,全都打上花括号,只有一句话也打上,多碍眼啊?然而经过实行这种编码规范几年之后,我并没有发现这种写法更加碍眼,反而由于花括号的存在,使得代码界限明确,让我的眼睛负担更小了。 * 合理使用括号,不要盲目依赖操作符优先级。利用操作符的优先级来减少括号,对于`1 + 2 * 3`这样常见的算数表达式,是没问题的。然而有些人如此的仇恨括号,以至于他们会写出`2 << 7 - 2 * 3`这样的表达式,而完全不用括号。 这里的问题,在于移位操作`<<`的优先级,是很多人不熟悉,而且是违反常理的。由于`x << 1`相当于把`x`乘以2,很多人误以为这个表达式相当于`(2 << 7) - (2 * 3)`,所以等于250。然而实际上`<<`的优先级比加法`+`还要低,所以这表达式其实相当于`2 << (7 - 2 * 3)`,所以等于4! 解决这个问题的办法,不是要每个人去把操作符优先级表给硬背下来,而是合理的加入括号。比如上面的例子,最好直接加上括号写成`2 << (7 - 2 * 3)`。虽然没有括号也表示同样的意思,但是加上括号就更加清晰,读者不再需要死记`<<`的优先级就能理解代码。 * 避免使用continue和break。循环语句(for,while)里面出现return是没问题的,然而如果你使用了continue或者break,就会让循环的逻辑和终止条件变得复杂,难以确保正确。 出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种: 1. 如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue。 2. 如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。 3. 有时候你可以把break替换成return,从而去掉break。 4. 如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。 下面我对这些情况举一些例子。 情况1:下面这段代码里面有一个continue: ~~~ List<String> goodNames = new ArrayList<>(); for (String name: names) { if (name.contains("bad")) { continue; } goodNames.add(name); ... } ~~~ 它说:“如果name含有'bad'这个词,跳过后面的循环代码……” 注意,这是一种“负面”的描述,它不是在告诉你什么时候“做”一件事,而是在告诉你什么时候“不做”一件事。为了知道它到底在干什么,你必须搞清楚continue会导致哪些语句被跳过了,然后脑子里把逻辑反个向,你才能知道它到底想做什么。这就是为什么含有continue和break的循环不容易理解,它们依靠“控制流”来描述“不做什么”,“跳过什么”,结果到最后你也没搞清楚它到底“要做什么”。 其实,我们只需要把continue的条件反向,这段代码就可以很容易的被转换成等价的,不含continue的代码: ~~~ List<String> goodNames = new ArrayList<>(); for (String name: names) { if (!name.contains("bad")) { goodNames.add(name); ... } } ~~~ `goodNames.add(name);`和它之后的代码全部被放到了if里面,多了一层缩进,然而continue却没有了。你再读这段代码,就会发现更加清晰。因为它是一种更加“正面”地描述。它说:“在name不含有'bad'这个词的时候,把它加到goodNames的链表里面……” 情况2:for和while头部都有一个循环的“终止条件”,那本来应该是这个循环唯一的退出条件。如果你在循环中间有break,它其实给这个循环增加了一个退出条件。你往往只需要把这个条件合并到循环头部,就可以去掉break。 比如下面这段代码: ~~~ while (condition1) { ... if (condition2) { break; } } ~~~ 当condition成立的时候,break会退出循环。其实你只需要把condition2反转之后,放到while头部的终止条件,就可以去掉这种break语句。改写后的代码如下: ~~~ while (condition1 && !condition2) { ... } ~~~ 这种情况表面上貌似只适用于break出现在循环开头或者末尾的时候,然而其实大部分时候,break都可以通过某种方式,移动到循环的开头或者末尾。具体的例子我暂时没有,等出现的时候再加进来。 情况3:很多break退出循环之后,其实接下来就是一个return。这种break往往可以直接换成return。比如下面这个例子: ~~~ public boolean hasBadName(List<String> names) { boolean result = false; for (String name: names) { if (name.contains("bad")) { result = true; break; } } return result; } ~~~ 这个函数检查names链表里是否存在一个名字,包含“bad”这个词。它的循环里包含一个break语句。这个函数可以被改写成: ~~~ public boolean hasBadName(List<String> names) { for (String name: names) { if (name.contains("bad")) { return true; } } return false; } ~~~ 改进后的代码,在name里面含有“bad”的时候,直接用`return true`返回,而不是对result变量赋值,break出去,最后才返回。如果循环结束了还没有return,那就返回false,表示没有找到这样的名字。使用return来代替break,这样break语句和result这个变量,都一并被消除掉了。 我曾经见过很多其他使用continue和break的例子,几乎无一例外的可以被消除掉,变换后的代码变得清晰很多。我的经验是,99%的break和continue,都可以通过替换成return语句,或者翻转if条件的方式来消除掉。剩下的1%含有复杂的逻辑,但也可以通过提取一个帮助函数来消除掉。修改之后的代码变得容易理解,容易确保正确。