iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 21
4

良好程式碼的優點大同小異。
不好的程式碼的糙點卻各有巧妙之處。

exception: try-catch + throw

很少人用過吧?
但是真正製作產品上,傳聞寫 try-catch 的 code 比寫正常邏輯還多是正常的。

介紹語法 Syntax

先介紹 throwError 的用法

function willHappenError() {
  //...
  throw Error("this is my error")
  //...
}

willHappenError ();

執行順序

當發生 exception (Error) 時,執行順序,就不是我們一般熟知那樣的順序,而是有點像是「有某種特定規律的 goto() 」的執行方式。

  1. 橘色: 主程式宣告完 willHappenError 後,就執行第 7 行
  2. 藍色: 跳到 willHappenError function 定義
  3. 黑色: 執行 willHappenError function 內容
  4. 紅色: 遇到 throw 將即將 throw 的物件產生並且 throw 出目前的 function scope。

由於呼叫 willHappenError 的 scope (global)沒有任何補捉機制,所以會自動再丟往 global 通知瀏覽器強制停止、顯示錯誤。

執行結果

注意

在 C++ 這種沒有垃圾搜集機制的語言中。
拋出來的會是 Exception 物件,而不是 Exception 指標。

那 try-catch 呢?

function happenError() {
  console.log(2)
  throw Error("this is my error")
  console.log(3)
}

try {
    console.log(1)
    happenError ();
    console.log(4)
}
catch(e) {
  console.log(5)
  console.log(e.message)
  console.log(6)
}
finally {
  console.log(7)
}

console.log(8)

執行順序

  1. 橘色: 主程式宣告完 willHappenError 後,就執行第 7 行
  2. 藍色: 第 9 跳到第 1 行 willHappenError function 定義
  3. 黑色: 執行 willHappenError function 內容
  4. 紅色: 遇到 throw 將即將 throw 的物件產生並且 throw 出目前的 function scope。
  5. 呼叫 willHappenError 的 scope (global) 有補捉機制 try-catch 所以會跳到 catch 並且執行裡面的 code
  6. 執行 finally, 等一下再介紹。
  7. 執行 try 後面的程式碼!!!
  8. 安全降落

執行結果

看到了嗎?這次就 safe 多了,程式都有執行,並且沒有出現紅字。

有些語言支援 finally 有些沒有

只要在 try 區塊中,發生 exception 就會執行 finally block 裡的 code。
通常順序會是 catch 區塊執行結束,才會執行 finally 而且一定會搭配使用。

這樣的語法要怎麼糙呢?

記得 過度依賴前置處理器 提過的 MACRO (前置處理器) 的寫法嗎?
絕對不要拿來裝 try-catch 原因如下

  1. C 語言沒有 try-catch,所以這些建議不適用於 C 語言
  2. C++ 有 try-catch ,而且不要用 MARCO ,所有 MARCO 的功能,C++ 都有適合的語法可以取代,包含 template 和 generic 的寫法。

絕對不要用 MARCO 把 try 和 catch 分開裝起來。

設計

#define START()\
  try {\

#define END()\
  }\
    catch(exception& e) {\
    log("Occur exception");\
    return ;\
  }\
    catch(...) {\
    log("Occur exception");\
    return ;\
  }\

實際使用

使用上其實和一般使用的習慣不同。

void function () {
  START()
  // do something...
  END()
}

要加分號嗎?
加了是不是算結束的語意?

void function () {
  START();
  // do something...
  END();
}

有一本書,針對例外處理的程式碼該如何編排做了相當的研究,不得不大推一下 《笑談軟體工程:例外處理設計的逆襲》

同作者的部落格中也有介紹幾個 try-catch 的壞味道[1]

  • Return Code(回傳碼)
  • Ignored Checked Exception(忽略受檢例外)
  • Ignored Exception(忽略例外)
  • Unprotected Main Program(未被保護的主程式)
  • Dummy Handler(虛設的處理者)
  • Nested Try Statement(巢狀Try敘述)
  • Spare Handler(備胎)
  • Careless Cleanup(粗心的資源釋放)

非常棒的書。推薦買來認識「不糙的例外處理,如何寫」

你的害怕: 巢狀式的 try-catch

這篇就介紹一下我自己遇到的案例與正確的認識吧。

再拿 過度依賴前置處理器 提過的 code

設計

#define BEGIN_TRANSACTION(transaction, status_code)\
  int status_code = ERROR_SUCCESS;\
  Transaction* transaction = 0;\
  try\
  {\
    transaction = new Transaction();\
    try\
    {\


#define END_TRANSACTION(transaction, status_code)\
  }\
  catch(...)\
  {\
      status_code = TRANSACTION_ERROR;\
      log("END_TRANSACTION Error");\
  }\
}\
catch(...)\
{\
}\
{\
  try\
  {\
      if( !transaction->Commit() )\
      {\
          if( status_code == SUCCESS )\
          {\
              status_code = TRANSACTION_ERROR;\
              log("END_TRANSACTION Error");\
              transaction->Rollback();\
          }\
      }\
  }\
  catch(...)\
  {\
      status_code = TRANSACTION_ERROR;\
      log("END_TRANSACTION Error");\
  }\
  \
  try\
  {\
      delete transaction;\
  }\
  catch(...)\
  {\
      status_code = TRANSACTION_ERROR;\
      log("END_TRANSACTION Error");\
  }\
}

先把 MARCO 和 try-catch 拿掉看看

int status_code = ERROR_SUCCESS;
Transaction *transaction = 0;
transaction = new Transaction();

// do something

if (!transaction->Commit()) {
  if (status_code == SUCCESS) {
    status_code = TRANSACTION_ERROR;
    log("END_TRANSACTION Error");
    transaction->Rollback();
  }
}

delete transaction;

這樣的 code 是不是擺在一起就好看多了?

「難道不能這樣寫 code 嗎?」

客戶產品上線,怕出現「意料之外」的情況。
造成無法回頭的問題。

「告訴你!你愈這樣,就愈容易遇到」

而且,還是可以加 try-catch

try {
  int status_code = ERROR_SUCCESS;
  Transaction *transaction = 0;
  transaction = new Transaction();
  
  // do something
  
  if (!transaction->Commit() && status_code == SUCCESS) {
    status_code = TRANSACTION_ERROR;
    log("END_TRANSACTION Error");
    transaction->Rollback();
  }

  delete transaction;
}
catch (...) {
  status_code = TRANSACTION_ERROR;
  log("END_TRANSACTION Error");
}

「難道你就不能...」(好了好了...)

你的焦慮 空白 catch

再拿剛剛那一段 MARCO 裡的 END_TRANSACTION()
不知道你有沒有仔細看裡面有一段

設計

#define END_TRANSACTION(transaction, status_code)\
//... code
catch(...)\
{\
  <- 空白的 catch 
}\
  <- 這裡沒有東西 單純用大括號括出 scope
{\
  //...other code
}

糙 code 讓你眼花瞭亂呀。
無法一眼看出來的就是糙!!

不知道出現什麼錯誤,所以使用 catch(...)
不知道要怎麼處理,所以使用空白的 catch block

如果非要用空白的 catch 不可,請加上註解!!!

另外,單純用大括號括出 scope ,我也不懂就什麼會寫出這種東西。

這樣的程式碼,還有什麼糙點呢?

如果寫程式需要這麼害怕和焦慮,請務必再重新熟悉語法,而不是依賴 try-catch 讓你的程式碼不顯示錯誤 (不是不出錯)

C++ 的捕捉任意 exception catch(...) 不要濫用

產品在 release mode 後要容錯,debug mode 時要突顯錯誤。

debug 就是要解決出錯的原因。但是這樣的程式碼還出現在 debug mode 要如何找到錯誤的原因呢?

還遇過,初始化由 try-catch 巧妙的容錯,造成無法好好修改的邏輯,真是可怕的回憶

C++ 使用 MARCO 進行條件編譯

catch (Exception e) {
  status_code = TRANSACTION_ERROR;
  log("END_TRANSACTION Error");
}
#ifndef _DEBUG  // release mode 再加上去
catch (...) {
  status_code = TRANSACTION_ERROR;
  log("END_TRANSACTION Error");
}
#endif

不要巢狀 try-catch

你有用過 goto() 嗎?就是發明巢狀後,才可以大幅順利的不使用它,你知道嗎?

這樣的結構,會讓你不知道要如何處理垃圾收集

try {
  try {

  } catch {
  
  }
} catch {
  try {

  } catch {
  
  }
}

如果一定要,就用 function 包起來,給它一個好命名。

正確的使用 try-catch

try-catch 要如何使用?

  1. 最重要的,一定要知道的就是,try-catchif-else
    遇到把它們搞混的工程師,只有想到「糙」而已。
  2. 第二件事,辨別「正常流程」和「錯誤流程」。
    「正常流程放一起,錯誤流程另外寫」的概念,把原本沒有 try-catch 中用 if 處理錯誤流程,獨立出來。因為錯誤流程常常有一致的處理流程與呈現方式
try {
  //正常
}
catch (...){
  // 出錯
}

「當你內心充滿焦慮時,一定要正面對決問題」
這是人生的問題呀

參考資料

[1]: 例外處理壞味道:將例外當作控制流程


上一篇
魔法般的 magic number
下一篇
git log 也可以糙!!
系列文
可不可以不要寫糙 code30

尚未有邦友留言

立即登入留言