有人在看了我的「如何將介面程式碼和功能程式碼分離(基於Delphi/VCL)」之後,提到一個問題,就是如何處理服務端的類別的錯誤。
在基於函數的結構中,我們一般使用函數傳回值來標明函數是否成功執行,並給出錯誤類型等資訊。於是就會有如下形式的代碼:
RetVal := SomeFunctionToOpenFile();
if RetVal = E_SUCCESSED then
.....
else if RetVal = E_FILENOTFOUND then
.....
else if RetVal = E_FILEFORMATERR then
.....
else then
.....
使用傳回錯誤代碼的方法是非常普遍的,但是使用這樣的方法有2個問題:
1.造成冗長、繁雜的分支結構(大量的if或case語句),使得控制流程變得複雜
2、可能會有沒有被處理的錯誤(函數呼叫者如果不判斷回傳值的話)
而異常是對於錯誤處理的物件導向的解決方案。它可以報告錯誤,但需要知道的是,並非由於錯誤而引發了異常,而只是因為使用了raise。
在Object Pascal中,拋出異常使用的是raise保留字。在任何時候(即使沒有錯誤發生),raise都會導致異常的發生。
異常可以使得程式碼從異常發生處立刻傳回,從而保護其下面的敏感程式碼不會被執行。透過異常從函數返回和正常從函數返回(執行到函數末尾或執行了Exit)對於拋出異常的函數本身來說是沒有什麼區別的。差別在於呼叫者處,經過異常回傳後,執行權會被呼叫者的try...except區塊所捕捉(如果它們存在的話)。如果呼叫者處沒有try...except區塊的話,將不會繼續執行後續語句,而是傳回更上層的呼叫者,直到找到能夠處理該異常的try...except區塊。異常被處理後,將繼續執行try...except區塊之後的語句,控制權就被留在了處理異常的這一層。當異常處理程序感覺對異常的處理還不夠完整時,需要更上層呼叫者繼續處理,可以重新拋出異常(使用簡單的raise;即可)將控制權交給更上層呼叫者。
如果根本沒有預設try...except區塊,則最終異常會被最外層的封裝整個程式的VCL的try...except區塊所捕獲。
因此,不會有不被處理的異常,換句話說,也就是不會有不被處理的錯誤(雖然錯誤和異常並不能劃等號)。這也是異常機制比使用傳回錯誤代碼方法的優越之處。另外,異常被拋出後,其控制流程的走向非常清晰明了,不會造成流程失去控制的情況。
舉個例子說明異常的工作機制,假設我們要開啟某種特定格式的檔案:
先定義兩個異常類別(從Exception繼承)
EFileNotFound = class(Exception);
EFileFormatErr = class(Exception);
假設Form1上有一個按紐,按下按紐即開啟檔案:
PRocedure TForm1.Button1Click(Sender: TObject);
begin
try
ToOpenFile();
except
on EFileNotFound do
ShowMessage('Sorry, I can't find the file');
on EFileFormatErr do
ShowMessage('Sorry, the file is not the one I want');
on E:Exception do
ShowMessage(E.Message);
end;
end;
以及開啟檔案的功能函數:
procedure ToOpenFile;
var RetVal:Integer;
begin
//Some code to openfile
RetVal := -1; //open failed
if RetVal = 0 then //success
Exit
else if RetVal = -1 then
Raise EFileNotFound.Create('File not found')
else if RetVal = -2 then
Raise EFileFormatErr.Create('File format error')
else //other error
Raise Exception.Create('Unknown error');
end;
程式中TForm1.Button1Click 呼叫ToOpenFile,並預設了對ToOpenFile可能拋出的異常處理的try...except。當然,也可以將TForm1.Button1Click 的異常處理程式碼進行簡化:
procedure TForm1.Button1Click(Sender: TObject);
begin
try
ToOpenFile();
except
ShowMessage('Open file failed');
end;
end;
使用異常解決了使用返回錯誤代碼方法存在的問題,當然,使用異常也不是沒有代價的。異常會增加程序的負擔,因此濫用異常也是不可取的。寫若干try...except和寫數以千計的try...except之間是有很大差別的。用Chalie Calverts的話來說就是:「在似乎有用的時候,就應該使用try...except塊。但是要試著讓自己對這種技術的熱情不要太過頭」。
另外,Object Pascal引進了獨特的try...finally結構。前面我說過,透過異常從函數返回和正常從函數返回是沒有什麼區別的。因此,函數中的堆疊中的局部對象,會自動被釋放,而堆中的對象則不會。而然,Object Pascal的物件模型是基於引用的,其存在於堆中,而非堆疊中。因此,有時我們在透過異常從函數傳回之前需要清理一些局域的物件資源。 try...finally正是解決這個問題的。
我改寫了以上的ToOpenFile 的程式碼,這次讓ToOpenFile過程中使用了一些資源,並在異常發生後(或不發生)從函數返回前都會釋放這些資源:
procedure ToOpenFile;
var RetVal: Integer;
Stream: TStream;
begin
//Some code to openfile
Stream := TStream.Create;
RetVal := -1; //open failed
try
if RetVal = 0 then //success
Exit
else if RetVal = -1 then
Raise EFileNotFound.Create('File not found')
else if RetVal = -2 then
Raise EFileFormatErr.Create('File format error')
else //other error
Raise Exception.Create('Unknown error');
finally
Stream.Free;
end;
end;
單步執行以上程式碼,可以看出,即使RetVal的值為0 時,執行Exit後,仍會執行finally中的程式碼,然後再從函數傳回。由此保證了局部資源的正確釋放。
try...except和try...finally的用途和使用場合是不同的,而許多初學者會將它們混淆。以下是筆者的一些個人認識:try...except一般用於呼叫者處捕獲所調用的函數所拋出的異常並進行處理。而try...finally一般用來拋出異常的函數本身進行一些資源清理工作。
物件導向程式設計提供了「異常」這種錯誤處理的方案。善而用之,會對我們的工作有好處,可以顯著改善所編寫程式碼的品質。
Nicrosoft([email protected])2001.7.25
原文出處:東日文件(http://www.sunistudio.com/asp/sunidoc.asp)