是返回錯誤碼,還是丟擲異常?說說我的選擇
昨晚翻了翻《 松本行弘的程式世界 》這本書,看到他對異常設計原則的講述,覺得頗為贊同。近期的面試,我有時也問類似的問題,但應聘者的回答大都不能令人滿意。有必要理一理,說說我是怎麼理解的,以及在程式設計實踐中如何做出合適的選擇。當然這只是一家之言,未必就是完全正確的。
首先,要明確一點的是,錯誤碼和異常,這兩者在程式的表達能力上是等價的。它們都可以向呼叫者傳達“與常規情況不一樣的狀態”。考慮使用哪一種,更多地是從API的設計、系統的效能指標、新舊程式碼的一致性這3個角度來考慮的(本文主要從API設計的角度著手)。
接下來,看一個使用返回錯誤碼的例子:
using namespace std; int strlen(char *string) { if (string == NULL) { return -1; } int len = 0; while(*string++ != \'\\0\') { len += 1; } int main(void) { int rc; char input[] = {0}; rc = strlen(input); if (rc == -1) { cout << \"Error input!\" << endl; return -1; } cout << \"strlen: \" << rc << endl; char *input2 = NULL; rc = strlen(input2); if (rc == -1) { cout << \"Error input!\" << endl; return -2; } cout << \"strlen: \" << rc << endl;
與之等價的使用異常的程式是:
using namespace std; int strlen(char *string) { if (string == NULL) { throw \"Invalid input!\"; } int len = 0; while(*string++ != \'\\0\') { len += 1; } int main(void) { char input[] = {0}; cout << \"strlen: \" << strlen(input) << endl; char *input2 = NULL; cout << \"strlen: \" << strlen(input2) << endl;
從以上兩個程式片段的對比中,不難看出使用異常的程式更為簡潔易懂。為什麼?
原因是:返回錯誤碼的方式,使得呼叫方必須對返回值進行判斷,並作相應的處理。這裡的處理行為,大部份情況下只是打一下日誌,然後返回,如此這般一直傳遞到最上層的呼叫方,由它終止本次的呼叫行為。這裡強調的是,“必須要處理錯誤碼“,否則會有兩個問題:1)程式接下來的行為都是基於不確定的狀態,繼續往下執行的話就有可能隱藏BUG;2)自下而上傳遞的過程實際上是語言系統出棧的過程,我們必須在每一層都記下日誌以形成日誌棧,這樣才便於追查問題。
而採用異常的方式,只管寫出常規情況下的邏輯就可以了,一旦出現異常情況,語言系統會接管自下而上傳遞資訊的過程。我們不用在每一層呼叫都進行判斷處理(不明確處理,語言系統自動向上傳播)。最上層的呼叫方很容易就可以獲得本次的呼叫棧,把該呼叫棧記錄下來就可以了。因此,使用異常能夠提供更為簡潔的API。
上述的例子還不是最絕的,因為錯誤碼和常規輸出值並沒有交集,那最絕的情況是什麼呢?錯誤碼侵入了或者說汙染了常規輸出值的值域了,這時只能通過其它的渠道返回常規輸出了。如:
using namespace std;
int get_avg_temperature(int day, int *result) {
if (day < 0) {
return -1;
}
*result = day;
int main(void) {
int rc;
int result;
rc = get_avg_temperature(1, &result);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -1;
}
cout << \"avg temperature: \" << result << endl;
rc = get_avg_temperature(-1, &result);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -2;
}
cout << \"avg temperature: \" << result << endl;
當然,如果能忍受低效率,也可以把錯誤碼和常規輸出捆到一個結構裡再返回,如下:
using namespace std;
typedef struct {
int rc;
int result;
} box_t;
box_t get_avg_temperature(int day) {
box_t b;
if (day < 0) {
b.rc = -1;
b.result = 0;
return b;
}
b.rc = day;
b.result = 0;
int main(void) {
box_t b;
b = get_avg_temperature(1);
if (b.rc == -1) {
cout << \"Error input!\" << endl;
return -1;
}
cout << \"avg temperature: \" << b.result << endl;
b = get_avg_temperature(-1);
if (b.rc == -1) {
cout << \"Error input!\" << endl;
return -2;
}
cout << \"avg temperature: \" << b.result << endl;
與之等價的使用異常的程式是:
using namespace std;
int get_avg_temperature(int day) {
if (day < 0) {
throw \"Invalid day!\";
}
int main(void) {
cout << \"avg temperature: \" << get_avg_temperature(1) << endl;
cout << \"avg temperature: \" << get_avg_temperature(-1) << endl;
return 0;
}
哪一個醜陋,哪一個優雅,我想應該不用我多說了。既然使用異常這麼好,那我們是不是乾脆全部使用異常算了?當然也不是。以下這個例子也使用了異常,但我們可以看到程式仍然比較冗長:
#include
#include
#include
using namespace std;
class database {
private:
map store;
public:
database() {
store[\"a\"] = 100;
store[\"b\"] = 99;
store[\"c\"] = 98;
}
int get(string key) {
map ::iterator iter = store.find(key);
if (iter == store.end()) {
throw \"No such user exception!\";
}
return iter->second;
}
};
int main(void) {
database db;
try {
cout << \"score: \" << db.get(\"a\") << endl;
} catch (char const *&e) {
cout << e << endl;
}
try {
cout << \"score: \" << db.get(\"d\") << endl;
} catch (char const *&e) {
cout << e << endl;
}
return 0;
}
與之等價的使用錯誤碼的程式如下:
#include
#include
#include
using namespace std;
class database {
private:
map store;
public:
database() {
store[\"a\"] = 100;
store[\"b\"] = 99;
store[\"c\"] = 98;
}
map ::iterator get(string key) {
return store.find(key);
}
inline map ::iterator not_exist() {
return store.end();
}
};
int main(void) {
database db;
map ::iterator iter;
iter = db.get(\"a\");
if (iter == db.not_exist()) {
cout << \"no such user!\" << endl;
} else {
cout << \"score: \" << iter->second << endl;
}
iter = db.get(\"d\");
if (iter == db.not_exist()) {
cout << \"no such user!\" << endl;
} else {
cout << \"score: \" << iter->second << endl;
}
在這個例子當中,使用異常並沒有帶來明顯的好處,該做的事情還得做。這種情況下,是選擇錯誤碼還是選擇異常要結合系統自身的需求和現有程式碼的情況了,當然,有時候這純粹是個人口味問題。接下來再舉一些例子:
使用錯誤碼的例子:
1、檢索資料時,對應某一鍵不存在相應的記錄的情況。這時應使用錯誤碼。這種情況要明確告訴呼叫方,並不是系統出異常了,而是資料確實不存在,請作出相應的處理。
2、查詢使用者時,輸入的使用者名稱並不存在的情況。這時應使用錯誤碼。這種情況要明確告訴呼叫方,並不是系統出異常了,而是使用者確實不存在,請作出相應的處理。
使用異常的例子:
1、讀取檔案時,檔案不存在的情況。(我要讀取檔案,我認為它是存在的(可能已經進行了相應的判斷),但現在檔案不存在了(可能中途被人刪除了),OK,丟擲異常,呼叫方不明確處理的話,系統異常終止)。
2、修改使用者資料時,使用者不存在的情況。(我要修改使用者資料,我認為使用者是存在的(可能已經進行了相應的判斷),但現在使用者不存在了(可能中途被人刪除了),OK,丟擲異常,呼叫方不明確處理的話,系統異常終止)。
模凌兩可的例子:
1、入棧,棧滿;出棧,棧空。
2、陣列越界。
3、除0錯。
4、連線時,網路出錯。
綜上所述,是返回錯誤碼還是丟擲異常,有以下3條規則:
規則1:本次發生的異常現象是不是真的非常罕見?非常罕見意味著發生該現象的機率非常低,呼叫方每次都處理將浪費大量的精力,但如果出現,系統應該明確終止,而不是繼續往下執行,所以使用異常;其它情況,參考規則2。
規則2:本次發生的異常現象是不是出乎意料之外?出乎意料之外意味著系統已經出問題了(狀態和預期不一致),被呼叫方沒辦法再往下執行了,而呼叫方如果不明確處理的話,系統也應該終止(因為系統狀態出問題了),所以使用異常;其它情況,參考規則3。
規則3:遇到模凌兩可的情況,則根據系統的效能需求(異常使用不當可能會造成系統抖動)、現有程式碼的情況(要保持一致的程式風格)以及開發人員的口味(怎麼舒服怎麼來)。