關於異常

今天看了 xuchaoqian的blog: http://xuchaoqian.com/?p=563 深有同感啊

關於異常,它的好處就是:

    1. 不需要在調用棧的每一 上判斷出錯 ,而只需要在關心錯誤的那一層處理就行

    2. 錯誤碼侵入了或者說污染了常規輸出值的值域了

如果是非常規情況,使用異常. 如文件不存在,網絡掛掉之類,而如果是常規情況,getsize之類,不要用異常.

 

 

xuchaoqian 的例子,裏面只有一層調用,看不出太大的優點

 

 

 

附原文:

 

是返回值(錯誤碼、特殊值),還是拋出異常?說說我的選擇

昨晚翻了翻《松本行弘的程序世界》這本書,看到他對異常設計原則的講述,覺得頗爲贊同。近期的面試,我有時也問類似的問題,但應聘者的回答大都不能令人滿意。有必要理一理,說說我是怎麼理解的,以及在編程實踐中如何做出合適的選擇。當然這只是一家之言,未必就是完全正確的。

在行文之前,我有一個觀點需要明確:錯誤碼和異常,這兩者在程序的表達能力上是等價的,它們都可以向調用者傳達“與常規情況不一樣的狀態”。因此,要使用哪一種,是需要從API的設計、系統的性能指標、新舊代碼的一致性這3個角度來考慮的。本文主要從API的設計着手,試圖解決兩個問題:1)爲什麼要使用異常?2)什麼時候應返回特殊值(注:這不是錯誤碼)而不是拋出異常?

好,先來看一個使用返回錯誤碼的例子:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;
int strlen(char *string) {
if (string == NULL) {
return -1;
}
int len = 0;
while(*string++ != '\0') {
len += 1;
}
return len;
}
int main(void) {
int rc;
char input[] = {0};
rc = strlen(input);
if (rc == -1) {
cout << "Error input!" << endl;
return -1;
}
cout << "String length: " << rc << endl;
char *input2 = NULL;
rc = strlen(input2);
if (rc == -1) {
cout << "Error input!" << endl;
return -2;
}
cout << "String length: " << rc << endl;
return 0;
}

 

與之等價的使用異常的程序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
int strlen(char *string) {
if (string == NULL) {
throw "Invalid input!";
}
int len = 0;
while(*string++ != '\0') {
len += 1;
}
return len;
}
int main(void) {
char input[] = {0};
cout << "String length: " << strlen(input) << endl;
char *input2 = NULL;
cout << "String length: " << strlen(input2) << endl;
return 0;
}

 

從以上兩個程序片段的對比中,不難看出使用異常的程序更爲簡潔易懂。爲什麼?

原因是:返回錯誤碼的方式,使得調用方必須對返回值進行判斷,並作相應的處理。這裏的處理行爲,大部份情況下只是打一下日誌,然後返回,如此這般一直傳遞到最上層的調用方,由它終止本次的調用行爲。這裏強調的是,“必須要處理錯誤碼“,否則會有兩個問題:1)程序接下來的行爲都是基於不確定的狀態,繼續往下執行的話就有可能隱藏BUG;2)自下而上傳遞的過程實際上是語言系統出棧的過程,我們必須在每一層都記下日誌以形成日誌棧,這樣才便於追查問題。

而採用異常的方式,只管寫出常規情況下的邏輯就可以了,一旦出現異常情況,語言系統會接管自下而上傳遞信息的過程。我們不用在每一層調用都進行判斷處理(不明確處理,語言系統自動向上傳播)。最上層的調用方很容易就可以獲得本次的調用棧,把該調用棧記錄下來就可以了。因此,使用異常能夠提供更爲簡潔的API。

上述的例子還不是最絕的,因爲錯誤碼和常規輸出值並沒有交集,那最絕的情況是什麼呢?錯誤碼侵入了或者說污染了常規輸出值的值域了,這時只能通過其它的渠道返回常規輸出了。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;
int get_avg_temperature(int day, int *result) {
if (day < 0) {
return -1;
}
*result = day;
return 0;
}
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;
return 0;
}

 

當然,如果能忍受低效率,也可以把錯誤碼和常規輸出捆到一個結構裏再返回,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
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;
return b;
}
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;
return 0;
}

 

與之等價的使用異常的程序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
int get_avg_temperature(int day) {
if (day < 0) {
throw "Invalid day!";
}
return day;
}
int main(void) {
cout << "Avg temperature: " << get_avg_temperature(1) << endl;
cout << "Avg temperature: " << get_avg_temperature(-1) << endl;
return 0;
}

 

哪一個醜陋,哪一個優雅,我想應該不用我多說了。異常機制雖好,但要是使用不當,設計出來的API反而會比較難用。舉個例子:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
int get(string key) {
map<string, int>::iterator iter = store.find(key);
if (iter == store.end()) {
throw "No such user!";
}
return iter->second;
}
};
int main(void) {
database db;
try {
cout << "Score: " << db.get("a") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
try {
cout << "Score: " << db.get("d") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
return 0;
}

 

這個例子也使用了異常,但卻是不恰當的使用。因爲,“找”這個操作只有兩個結果:要麼“找到”,要麼“沒找到”。換句話說,“沒找到“也是一種常規輸出值。一旦拋出常規輸出值,那在調用鏈上的所有層次裏都需要捕獲該異常並進行處理,那麼使用異常的初衷和好處也就消失了。實踐中,在這種查找類的功能裏,如果沒找到相應記錄,一般是通過返回一個特殊的值來告知調用方,比如:NULL、特殊的對象(如iterator)、特殊的整數(如EOF)等等(爲什麼?一是使用異常沒帶來什麼好處,二是邏輯統一可能爲後續處理帶來便利)。因此,上述例子可以改造爲:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
map<string, int>::iterator get(string key) {
return store.find(key);
}
inline map<string, int>::iterator end_iterator() {
return store.end();
}
};
int main(void) {
database db;
map<string, int>::iterator iter;
iter = db.get("a");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
iter = db.get("d");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
return 0;
}

 

接下來再舉一些例子:

使用特殊值的例子:
1、檢索數據時,對應某一鍵不存在相應的記錄的情況。
2、判斷是與否。

使用異常的例子:
1、讀取文件時,文件不存在的情況。
2、修改用戶資料時,用戶不存在的情況。
3、參數出錯。
4、數組越界。
5、除0錯。
6、入棧,棧滿;出棧,棧空。
7、網絡錯誤。

綜上所述,本文的結論是:

1、異常能提供更爲簡潔的API,並且能更早地發現隱藏的BUG。如有可能,要儘量採用。
2、不要拋出原本屬於返回值值域裏的值,一般是直接返回特殊值。經典使用場景是查找和判斷。

—The end.

 

 

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