異常不可用於邏輯處理
異常是錯誤處理,但是不可以用於邏輯處理,假設我們封裝了一個MsgQueue,這個類負責收集由服務端一條接受線程收集客戶端消息,另一條工作線程負責取出消息,並對消息進行處理。
class Msg {...};
class MsgQueue {
public:
void put(Msg msg) throw (std::length_error);
Msg get() throw (std::out_of_range);
.....
}
這里定義了MsgQueue的兩個方式,分別是收集客戶端消息,然后保存消息的put方法,還有處理客戶端消息,取出客戶消息的get方法。
對於put方法,由於內存有限,我們不可能讓這個MsgQueue無限度的保存消息,所以當保存的消息過多,無法再保存客戶端消息時,我們拋出std::length_error異常。
對於get方法,假設queue里沒有任何元素了,我們要告訴調用者queue是空的,但是這里無法返回一個空元素,所以我們的做法就是拋出一個std::out_of_range(STL的queue當queue是空時,調用front()方法居然可以取出元素,不拋出任何異常)。
工作線程的部分代碼如下:
Msg msg;
while (true) {
try {
msg = msgQueue.get(); //取出客戶端消息
handle(msg); //處理客戶端消息
} catch (std::out_of_range) {
sleep(1);
}
}
msgQueue由工作線程和工作線程共享,工作線程不斷輪詢,嘗試msgQueue是否可以取出消息,不可以表示沒有新的消息的到來,就等待一秒,可以取出消息就進行處理。
這段代碼的問題在於把異常用於邏輯處理了,msgQueue為空,這個場景是正常場景,因為不可能每個時刻都有消息過來,異常是用於錯誤處理的,所以合適的做法我們是應該先判斷msgQueue是否為空,不為空,我們才調用msgQueue.get()取出消息。
class MsgQueue {
public:
bool isEmpty();
void put(Msg msg) throw (std::length_error);
Msg get() throw (std::out_of_range);
.....
}
添加判斷queue是否為空的成員函數后,工作線程的代碼修改如下:
Msg msg;
while (true) {
if (msgQueue.isEmpty()) {
sleep(1);
continue;
}
msg = msgQueue.get(); //取出客戶端消息
handle(msg); //處理客戶端消息
}
這樣代碼就簡潔明了很多,沒有像try {} catch{}導致成塊代碼的出現,尤其是層次一多,代碼很難一目了然。
作為MsgQueue不僅要考慮調用者各個調用場景,應該提供判斷isEmpty()之類的方法,避免調用者在做邏輯處理時,不得去用異常捕獲,寫出一些難以理解的代碼。
當局部的控制可以滿足我們的業務時,就不要使用異常處理了,請用異常做錯誤處理。
何時該使用try和catch
異常適用於哪些錯誤處理呢,哪里我需要try {} catch{}呢,例如網絡異常,文件IO異常這些典型的錯誤,從一個服務器中,讀取服務器的響應,可能會出現網絡中斷,這樣的處理代碼是合適使用try {} catch{}處理的。
try {
writeRequestToServer();
readResponseFromServer();
} catch (NetworkError) {
//對網絡錯誤進行處理,例如像瀏覽器說服務器無法連接
}
例如像文件IO,我們讀寫的過程中,如果真的那么倒霉,遇到硬盤壞道,也必須做處理,一個健壯的程序是要考慮任何能想到的錯誤的
try {
writeFileToDisk();
} catch (IO_Error) {
//假設是IO壞道,一般能做的處理就是提示用戶說磁盤或者分區損壞了
}
這些都是錯誤處理,是使用異常try {} catch{}的場景,看代碼人的一看就知道try{}里的代碼可能會發生錯誤,出現錯誤是非法場景,但是又不能完全避免。
總結
try{} catch{}這些錯誤處理語句,能少用就少用,代碼成塊出現,很容易讓代碼變得難以閱讀,如果你用異常來做邏輯處理,代碼中出現try{} catch{}的概率會大大提高,導致代碼可閱讀性很差。
例如當if else的代碼塊有三層以上
if (...) {
if (...) {
if (....) {
if (...) {
....
}
}
}
}
這樣的代碼閱讀性很差(還沒有加else,否則更暈),if的層次超過了3層,我就覺得代碼該重構了,因為我覺得就算是這段代碼的開發者,過了一個星期后,要看看這段代碼,重新梳理邏輯,至少要一個多小時,如果你重構了,你只需要十分鍾就可以看懂代碼邏輯了(題外話了)
使用try {} catch {}會導致代碼塊的層次增加,代碼可閱讀性變差,公認的做法是用來做錯誤處理,如果用來做邏輯處理,會帶來沒有必要的麻煩,請盡量避免使用。
