程序應該怎麼寫。
轉載地址:http://www.cocoachina.com/programmer/20151125/14410.html?utm_source=tuicool&utm_medium=referral
1. 避免寫太長的函數。如果發現函數太大了,就應該把它拆分成幾個更小的。通常我寫的函數長度都不超過50行,那正好是我的筆記本電腦屏幕所能容納的代碼的行數。這樣我可以一目瞭然的看見一個函數,而不需要滾屏。50行並不是一個很大的限制,因爲函數裏面比較複雜的部分,往往早就被我提取出去,做成了更小的函數,然後從原來的函數裏面調用。所以我寫的函數大小一般遠遠不足50行。
有些人不喜歡使用小的函數,因爲他們想避免函數調用的開銷,結果他們寫出幾百行之大的函數。這是一種歷史遺留的錯覺。現代的編譯器都能自動的把小的函數內聯(inline)到調用它的地方,所以根本不產生函數調用,也就不會產生任何多餘的開銷。
同樣的一些人,也愛使用宏(macro)來代替小函數,這也是一種歷史遺留的錯覺。在早期的C語言編譯器裏,只有macro是靜態“內聯”的,所以他們使用宏,其實是爲了達到內聯的目的。然而能否內聯,其實並不是宏與函數的根本區別。宏與函數有着巨大的區別(這個我以後再講),應該儘量避免使用宏。爲了內聯而使用宏,其實是濫用了宏,這會引起各種各樣的麻煩,比如使程序難以理解,難以調試,容易出錯等等。
2. 每個函數只做一件簡單的事情。有些人喜歡製造一些“通用”的函數,既可以做這個又可以做那個,然後他們傳遞一個參數來“選擇”這個函數所要做的事情。這種“複用”其實是有害的。如果一個函數可能做兩種不一樣的事情,最好就寫成兩個不同的函數,否則這個函數的邏輯就不會很清晰,容易出現錯誤。
避免使用continue和break。循環語句(for,while)裏面出現return是沒問題的,但是如果使用了continue或者break,就會讓循環的邏輯和終止條件變得複雜,難以確保正確。如果只有一個continue或者break也許還好,但是如果你的循環語句裏面出現了多個continue或者break,你就該考慮改寫整個循環了。
出現continue或者break的原因,往往是對循環要執行的邏輯沒有想得很清楚。因爲如果你考慮周全了,應該是幾乎不需要continue或者break的。改寫循環的辦法有多種,你也許可以把複雜的部分提取出來,做成函數調用,或者把它變成一個沒有continue或者break的循環結構。
舉一個例子。下面這段代碼裏面有一個continue:
1
2
3
4
5
6
7
8
|
List goodNames = new ArrayList<>(); for (String name: names) { if (name.contains( "bad" )) { continue ; } goodNames.add(name); ... } |
它說:“如果name含有'bad'這個詞,跳過後面的循環代碼……” 注意,這是一種“負面”的描述,它不是在告訴你什麼時候“做”一件事,而是在告訴你什麼時候“不做”一件事。爲了知道它到底在幹什麼,你必須搞清楚continue會導致哪些語句被跳過了,然後腦子裏把邏輯反個向,你才能知道它到底想做什麼。這就是爲什麼含有continue和break的循環不容易理解,它們依靠“控制流”來描述“不做什麼”,“跳過什麼”,結果到最後你也沒搞清楚它到底“要做什麼”。
其實,我們只需要把continue的條件反向,這段代碼就可以很容易的被轉換成等價的,不含continue的代碼:
1
2
3
4
5
6
7
|
List goodNames = new ArrayList<>(); for (String name: names) { if (!name.contains( "bad" )) { goodNames.add(name); ... } } |
goodNames.add(name);和它之後的代碼全部被放到了if裏面,多了一層縮進,然而continue卻沒有了。你再讀這段代碼,就會發現更加清晰。因爲它是一種更加“正面”地描述。它說:“在name不含有'bad'這個詞的時候,把它加到goodNames的鏈表裏面……”
再舉一個例子:
1
2
3
4
5
6
7
8
9
10
|
public boolean hasBadName(List names) { boolean result = false ; for (String name: names) { if (name.contains( "bad" )) { result = true ; break ; } } return result; } |
這個函數檢查names鏈表裏是否存在一個名字,包含“bad”這個詞。它的循環裏包含一個break語句。這個函數可以被改寫成:
1
2
3
4
5
6
7
8
|
public boolean hasBadName(List 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%含有複雜的邏輯,但也可以通過提取一個幫助函數來消除掉。修改之後的代碼變得容易理解,容易確保正確。
另外,try { ... } catch裏面,應該包含儘量少的代碼。比如,如果foo和bar都可能產生異常A,你的代碼應該儘可能寫成:
1
2
3
4
5
6
|
try { foo(); } catch (A e) {...} try { bar(); } catch (A e) {...} |
而不是
1
2
3
4
|
try { foo(); bar(); } catch (A e) {...} |
第一種寫法能明確的分辨是哪一個函數出了問題,而第二種寫法全都混在一起。明確的分辨是哪一個函數出了問題,有很多的好處。比如,如果你的catch代碼裏面包含log,它可以提供給你更加精確的錯誤信息,這樣會大大地加速你的調試過程。