代碼的規範,王垠,帶你少走點彎路。

程序應該怎麼寫。

轉載地址: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,它可以提供給你更加精確的錯誤信息,這樣會大大地加速你的調試過程。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章