程序語言都喜歡標新立異,提供這樣那樣的“特性”,然而有些特性其實并不是什么好東西。很多特性都經不起時間的考驗,最后帶來的麻煩,比解決的問題還多。很多人盲目的追求“短小”和“精悍”,或者為了顯示自己頭腦聰明,學得快,所以喜歡利用語言里的一些特殊構造,寫出過于“聰明”,難以理解的代碼。
并不是語言提供什么,你就一定要把它用上的。實際上你只需要其中很小的一部分功能,就能寫出優秀的代碼。我一向反對“充分利用”程序語言里的所有特性。實際上,我心目中有一套最好的構造。不管語言提供了多么“神奇”的,“新”的特性,我基本都只用經過千錘百煉,我覺得值得信奈的那一套。
現在針對一些有問題的語言特性,我介紹一些我自己使用的代碼規范,并且講解一下為什么它們能讓代碼更簡單。
* 避免使用自增減表達式(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%含有復雜的邏輯,但也可以通過提取一個幫助函數來消除掉。修改之后的代碼變得容易理解,容易確保正確。