Delphi的例外處理 TRY IF NOT(Delphi_KTV=危險建築) THEN 上Delphi KTV高歌一曲; EXCEPT ON 酒醉鬧事 DO 呼叫警務人員處理; ON 發生地震 DO 緊急疏散人員至安全地點; ON 發生火災 DO BEGIN 啟動滅火設備與消防設施; 緊急疏散人員至安全地點; 呼叫消防隊展開救援行動; END; ELSE 發出警報 "發生不明狀況,請立即疏散"; END; 『由於我們的例外處理機制, 您可以安全地在此歡唱。』 安全等於事前的預防? 事實上, 通常我們很難去考慮所有的狀況而預防意外發生! 因此, 在Delphi的世界中, 安全有了多一道防護, 就是例外的處理機制: try…except…end及try…finally…end。 錯誤控制的策略 現在先問自己一個問題, 在知道例外處理機制之前您是如何控制錯誤的? 現在假設您想要將Memo1(一個Tmemo物件)的內容儲存到網路上的F:\Backup\Autoexec.bak , 傳統的做法可能會像下面這段程式碼: procedure TForm1.Button1Click(Sender: TObject); begin {判斷F:\是否存在} if DirectoryExists('F:\') then begin {建立子目錄} ForceDirectory('F:\Backup'); {檢查子目錄是否存在} if DirectoryExists('F:\Backup') then begin {先刪除舊檔,以便判斷存檔是否成功} if FileExists('F:\Backup\Autoexec.bak') then DeleteFile('F:\Backup\Autoexec.bak'); {存檔} Memo1.Lines.SaveToFile('F:\Backup\Autoexec.bak'); end else ShowMessage('無法建立子目錄!'); {判斷存檔是否成功} if FileExists('F:\Backup\Autoexec.bak') then ShowMessage('存檔成功!'); else ShowMessage('檔案無法儲存!'); end else ShowMessage('網路磁碟機不存在!'); end; 上面這段程式碼會先偵測網路磁碟機是否存在, 如果存在才建立子目錄, 接著判斷子目錄是否建立成功, 如果子目錄存在才開始存檔…最終的目的只是要以我們的方式告訴使用者存檔是否成功。但事實上, 上面這段程式碼執行失敗的機率很大, 比如說使用者沒有寫入的權限, 或者該檔案是唯讀的(ReadOnly)又或者磁碟機滿了都會造成存檔失敗, 於是CPU的控制權就會跳脫了Button1Click而交給Application物件, 然後會出現系統預設的錯誤訊息, 最後會發生什麼事則難以預期了。 在現實世界中, 有很多狀況是我們無法事先就能做出周詳的考慮而避免錯誤, 那麼, 既然錯誤是難以避免的何不等錯誤發生時再來處理呢? 例外處理機制就是採用這種策略! 請比較下面的程式碼: procedure TForm1.Button1Click(Sender: TObject); begin try ForceDirectory('F:\Backup'); {建立子目錄} Memo1.Lines.SaveToFile('F:\Backup\Autoexec.bak') {存檔} ShowMessage('存檔成功!'); except ShowMessage('檔案無法儲存,請通知系統人員處理!'); end; end; 這段程式碼完全不作任何的判斷, 但是它採用了例外處理機制來達到我們的目的, 而程式碼則是如此簡潔! 任何在try…except區塊中的程式碼如果在執行過程中發生了錯誤, 它就會立即跳到except…end區塊中執行。因此不論是建立子目錄的過程或是存檔的過程中發生了任何型態的錯誤都能秀出一段漂亮的中文訊息, 而程式也能繼續的執行下去。 比較之下, 例外處理機制以簡潔有力的方式提供了安全上的防護, 如此便宜又大碗的事您還猶豫什麼呢? 認識例外處理機制 在討論例外處理機制之前, 我們必須先知道『錯誤』與『例外』的關係:簡單的說, 例外是程式發生錯誤時所產生的物件。 在此所謂『錯誤』是指『執行時期的錯誤』也就是Runtime Errors, Runtime Errors發生在程式執行時期所遇到的一些事先定義好的錯誤狀況。而例外--Exception--則是發生Runtime Errors時所產生的物件。Delphi則提供一套語法用以控制與產生例外物件( Exceptions), 這就是『例外處理機制』。 Delphi已經儘量地將所有可能發生的錯誤轉換成例外, 而一但例外產生了就會持續到這個例外被處理或者是程式結束為止。 到此為止, 您可能仍然心存疑問: 錯誤可能發生的範圍為何? 例外又是如何產生的呢? 錯誤可能發生在硬體中斷、作業系統、RTL(Run-Time Library)、VCL (Visual Component Library) 與應用程式中, 而例外則是作業系統、RTL、VCL或應用程式中偵測到錯誤時刻意產生的物件, 大體而言, 作業系統、RTL、VCL都會適當地處理與產生例外, 而最後未處理的例外才會傳遞到應用程式中。 因此如果命令是由上向下傳達的話, 那麼例外就是由下往上傳遞的, 這與真實世界的『回報制度』很像;舉例來說, 假設您的角色是應用程式, 您的直屬部下是VCL, VCL的直屬部下是RTL, RTL 的直屬部下是作業系統… 當您下達一個讀取檔案的命令時(例如Memo1.Lines.LoadFromFile),命令會一層一層往下傳達, 最後傳達給作業系統, 它會驅動硬體裝置來讀取資料, 此時如果作業系統偵測到了錯誤, 它會將此錯誤資訊包裝起來以產生一個例外,並將此例外回報給它的直屬長官-RTL, RTL收到此例外後會做一些適當的處理, 並將無法處理的錯誤再度產生新的例外往上回報, 如此層層回報最後傳遞到您的手中讓您來處理。 例外處理的語法 例外處理的基本語法有下列幾種: try <保護區塊> except <例外處理區塊> end; try <保護區塊> finally <必然執行的區塊> end; try <保護區塊> except on <Exception類別> do <命令句> ; on <Exception類別> do <命令句> ; else <命令句列表> end; try <保護區塊> except on <識別字>:<Exception類別> do <命令句> ; on <識別字>:<Exception類別> do <命令句> ; else <命令句列表> end; 基本的try…except 在女男平等的社會中, 即使忽略性別, 男女依舊有別… try <保護區塊> except <例外處理區塊> end; 在大多數的狀況下, 您可以忽略例外的類別, 而只專注於保護區塊中產生例外時的處理程序; 只要程式跳到例外處理區塊執行時, 我們一定知道是保護區塊中的那幾行程式碼產生了例外, 因此即使不知道例外的類別, 也必然知道產生例外的原因。舉例來說, 在Form1上放了兩個TTable元件與一個TButton元件, 我們希望按下Button1之後, 會將Table1與Table2以C:\NewDB的路徑開啟資料表格, 如果開啟失敗就顯使一段訊息提醒使用者, 同時將資料庫的路徑指定到C:\OldDB然後再嘗試著開啟一次: unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DB, DBTables, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Table1: TTable; Table2: TTable; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); begin Table1.Close; Table2.Close; Table1.DatabaseName:='C:\NewDB'; Table2.DatabaseName:='C:\NewDB'; try {保護區塊} Table1.Open; Table2.Open; except {例外處理區塊} ShowMessage('因為無法開啟新的資料庫,將採用舊的資料庫!'); Table1.DatabaseName:='C:\OldDB'; Table2.DatabaseName:='C:\OldDB'; Table1.Open; Table2.Open; end; end; end. 這段程式碼中, 為何我們可以如此篤定當程式跳到except…end的例外處理區段時, 一定是Table1或Table2在Open的過程中出了問題? 答案就是try…except保護區塊中我們只保護了Table1.Open與Table2.Open兩個動作; 事實上, 我們並不知道發生例外時它的類別是什麼, 但是我們可以確定例外發生的原因是在於Open的過程中! 如果我們想進一步的知道:到底是Table1或者是Table2的Open出了問題? 只要適當地調整一下保護區塊的範圍即可。上面的程式碼可以如下修改以達到這個目的: procedure TForm1.Button1Click(Sender: TObject); begin Table1.Close; Table2.Close; Table1.DatabaseName:='C:\NewDB'; Table2.DatabaseName:='C:\NewDB'; {開啟Table1} try Table1.Open; except {Table1的例外處理} ShowMessage('無法開啟Table1的資料庫,將切換至舊的資料庫!'); Table1.DatabaseName:='C:\OldDB'; Table1.Open; end; {開啟Table2} try Table2.Open; except {Table2的例外處理} ShowMessage('無法開啟Table2的資料庫,將切換至舊的資料庫!'); Table2.DatabaseName:='C:\OldDB'; Table2.Open; end; end; 再強調一次, 即使不知道例外的類別, 也必然能夠知道產生例外的原因! 因此, 只要您善用這節所介紹的基本技巧, 幾乎可以解決百分之九十的錯誤控制問題。 忽略錯誤 為了顧全大局, 常常我們可以寬容錯誤… try <保護區塊> except end; 在某些的狀況下, 您不需要知道到底發生了什麼錯誤, 而您真正要的只是『試試看, 不行就算了!』。例如, 在OnDestroy事件中嘗試著將Log寫入檔案, 因為OnDestroy事件發生時, 無論如何程式都要結束那麼此時發生的任何錯誤都應該忽略。以確保程式能正常結束。程式如下: procedure TForm1.FormDestroy(Sender: TObject); begin try Log.SaveToFile(LogFileName); except end; Log.free; end; 注意到了嗎? except…end之間沒有任何的程式碼, 這代表當try…except保護區塊中發生了任何錯誤都將忽略, 因此可以確保Log.free可以執行。許多背景執行的應用程式也都會應用這種技巧來確保程式的正常執行。 再舉一個常見的例子, 假設我們需要在Form放一個FileListBox以便讓使用者點選檔案然後進行一些處理… 然而常常我們會希望, 在這個Form顯示時, FileListBox會先跳到一個預設的路徑: procedure TForm1.FormShow(Sender: TObject); begin {為FileListBox1設定一個預設的路徑} try FileListBox1.Directory:=aDefaultDir; except {如果路徑不存在,就算了} end; end; 由於如果您將一個不存在的路徑指定到 FileListBox1.Directory 那就會發生一個例外, 上面的範例則是將此例外忽略了, 因為這種例外對程式的執行並不會造成任何影響。 好了現在讓我們利用這種技巧來寫一個實用的function - DirWriteable它可以測試某個目錄是否可以讀寫, 這在判斷網路磁碟機的權限是非常有用的: function DirWriteable(dir:string):Boolean; var tmp:string; F: TextFile; begin {先預設傳回 FALSE} Result:=False; {計算測試用的暫時檔名} if dir[Length(dir)]<>'\' then dir:=dir+'\'; tmp:=dir+'_TEMP_.$$$'; try {測試檔案讀寫} AssignFile(F,tmp); Rewrite(F); CloseFile(F); DeleteFile(tmp); {如果上面的動作都成功才傳回 TRUE} Result:=True; except end; end; 資源保護最佳策略try…finally 有借有還, 再借不難! try <保護區塊> finally <必然執行的區塊> end; 有時候, 您配置了一塊記憶體, 結果不慎在運用的過程中發生了例外, 您該如何將此記憶體資源釋放掉? 這樣的問題在實際應用中層出不窮, 例如動態建立物件與動態配置記憶體、檔案與資料記錄的鎖定等等, 要求系統資源是您的權利, 將系統資源如數奉還則是您的義務! 但是該如何將以取得的資源即使是在例外產生的狀況下也能安全的釋放呢? 的確, 資源保護是一個重要的課題。 在典型的應用上, 必須受到保護的資源有: 檔案、記憶體、物件、Windows資源。就先以物件為例吧, 假設現在我們想要從外部檔案-C:\User.cfg讀入UserName的設定, 如果您的程式如下面的寫法就會有問題了: procedure TForm1.Button1Click(Sender: TObject); var strs:TStringList; begin strs:=TStringList.Create; {建立TStringList物件至 strs} strs.LoadFromFile('C:\User.cfg'); {將C:\User.cfg讀入strs} Edit1.Text:=strs.Values['UserName']; {讀取 'UserName' 設定} strs.free;{釋放strs} end; 程式中如果C:\User.cfg檔案不存在時, strs.LoadFromFile('C:\User.cfg') 就會發生例外, 而造成strs.free不會被執行, 因而strs建立物件時所分配的記憶體並不會被釋放! 在Delphi的世界中要克服這樣的問題是非常容易的; try…finally…end結構可以輕易的解決這個問題, 請參考下例: procedure TForm1.Button1Click(Sender: TObject); var strs:TStringList; begin strs:=TStringList.Create; try strs.LoadFromFile('C:\Test.ini'); Edit1.Text:=strs.Values['PASSWD']; finally strs.free; end; end; 同樣地try…finally之間是保護區塊, 在此區塊中的程式碼都會受保護, 最後無論保護區塊中有無例外產生都一定會跳到finally…end中執行, 而且例外並不會被消滅掉。因此上面這段程式碼可以保證strs.free一定會被執行。 例外只有兩種狀況下會被消滅: 一是例外被處理後, 一是程式結束後。在try…finally…end的語法中, finally…end區塊並不處理例外; 因此如果try…finally區塊中產生了例外, 執行完finally…end之後, 這個例外並不會被消滅, 它會一直持續到被處理之後才會消失。 OK, 說到這裡, 您應該發現之前我們寫的function - DirWriteable有一個嚴重的缺點, 就是檔案關閉的問題。應該改寫如下: function DirWriteable(dir:string):Boolean; var tmp:string; F: TextFile; begin {先預設傳回 FALSE} Result:=False; {計算測試用的暫時檔名} if dir[Length(dir)]<>'\' then dir:=dir+'\'; tmp:=dir+'_TEMP_.$$$'; try {測試檔案讀寫} AssignFile(F,tmp); try Rewrite(F);{測試寫入動作} finally CloseFile(F);{無論如何都必須將檔案關閉} end; DeleteFile(tmp);{刪除檔案} {如果上面的動作都成功才傳回 TRUE} Result:=True; except end; end; 如果您對上面的程式中巢狀的try…try…有問題, 請先不要急, 後面會有詳細的介紹。 處理特定的例外 - try…except on… 在Delphi中已經預先定義好了許多例外的類別(classes) , 這可以讓我們在例外處理的程序中獲得更準確更詳盡的錯誤資訊; 換句話說, 在處理例外時, 我們除了可以知道產生例外的原因外, 也可以知道例外的類別及訊息等資訊。 try <保護區塊> except on <Exception類別> do <命令句> ; on <Exception類別> do <命令句> ; else <命令句列表> end; 上面的語法讓我們可以只處理某些類別的例外, 而不同類別的例外可以有其特定的處理程序。請先看下面這段程式碼: procedure TForm1.Button1Click(Sender: TObject); var vINT:Integer; vFLOAT:double; begin vINT:=StrToInt(Edit1.Text); vFLOAT:= 1/vINT; Label1.Caption:=FloatToStr(vFLOAT); end; 這是一個求倒數的程序, 它會將使用者輸入至Edit1中的字串先轉換成整數, 然後顯示此數的倒數。這個簡單的程式碼, 最容易產生兩種例外: O 如果使用者輸入了一個不正確的整數字串, 如: '01B' 或 '2.3' 時會發生轉換上的錯誤, 而產生一個EConvertError的例外。 O 如果使用者輸入了 '0' 時會發生除以零的錯誤, 而產生一個EZeroDivide的例外。 當然除了這兩種例外也可能發生其他的例外, 例如如果程式某階段將Edit1釋放(Free)掉了, 或者因為其他因素造成Windows Resource不足而無法繼續等等, 但是無論如何, 我們現在只想控制EConvertError與EZeroDivide兩種例外, 那麼程式碼可以如下撰寫: procedure TForm1.Button1Click(Sender: TObject); var vINT:Integer; vFLOAT:double; begin try vINT:=StrToInt(Edit1.Text); vFLOAT:= 1/vINT; except on EConvertError do begin ShowMessage('輸入的整數值不正確!'); vFLOAT:=0.0; end; on EZeroDivide do vFLOAT:=0.0; end; Label1.Caption:=FloatToStr(vFLOAT); end; 如果我們也想對其他的例外稍作控制, 則可將程式碼改寫如下: procedure TForm1.Button1Click(Sender: TObject); var vINT:Integer; vFLOAT:double; begin try vINT:=StrToInt(Edit1.Text); vFLOAT:= 1/vINT; except on EConvertError do begin ShowMessage('輸入的整數值不正確!'); vFLOAT:=0.0; end; on EZeroDivide do vFLOAT:=0.0; else ShowMessage('發生了不明的錯誤!'); vFLOAT:=0.0; end; Label1.Caption:=FloatToStr(vFLOAT); end; 當發生了EConvertError與EZeroDivide以外的例外時, 程式的控制權就會跳到else的區段去執行, 因此所有的例外都可以在掌控之中。 也許讀者開始會擔心, Delphi定義了這麼多的例外, 到底什麼狀況會產生什麼樣的例外呢? 這裡提供一個簡單的方法可以在Delphi 2.0整合開發環境下的執行時期得知例外的類別: O 首先在Delphi 2.0整合開發環境下的主選單中, 選擇Tools然後點選Options… 此時會出現一個參數設定的對話盒。 O 選擇References頁, 在Debugging的方框中, 將Break on Exception的核對方塊打勾, 然後按OK。 現在, 只要程式執行中發生例外時, 無論您是否有對此例外作控制, Delphi都會告訴您例外的類別與其訊息。 善用例外的樣例(Instance) 事實上, 如果善用例外的樣例(Instance), 有時候程式可以寫得更簡單, 引用樣例的語法如下: try <保護區塊> except on <識別字>:<Exception類別> do <命令句> ; on <識別字>:<Exception類別> do <命令句> ; else <命令句列表> end; 假如上面的範例中, 我們希望將所有的例外訊息寫到Memo1中, 則程式碼則可以這般改寫: procedure TForm1.Button1Click(Sender: TObject); var vINT:Integer; vFLOAT:double; begin try vINT:=StrToInt(Edit1.Text); vFLOAT:= 1/vINT; except on Err : Exception do begin vFLOAT:=0.0; Memo1.Lines.Add(Err.Message); if (Err is EConvertError) or (Err is EZeroDivide) then ShowMessage('錯誤的整數值'); end; end; Label1.Caption:=FloatToStr(vFLOAT); end; 其中Err是一個識別字, 代表一個Exception類別的樣例, 由於Delphi預先定義好的例外都是由Exception類別繼承下來的, 因此這個Err樣例可以容納得下所有的例外。 如果您只想控制浮點數運算方面的例外, 那麼上面的 : on Err: Exception do … 應改改寫成 on Err: EMathError do … 這樣, 此例外控制區段, 只會控制到EMathError及所有繼承自EMathError的例外, 如: EZeroDivide, EInvalidOp, EOverflow, EUnderflow等。 例外的再發生 -- raise 在Delphi中每一個例外都有一個預設的例外處理程序, 這個預設的例外處理程序只是單純地將錯誤訊息用一個對話盒顯示出來, 如下圖所示: 因此, 如果您的例外處理只是顯示一些中文化的訊息, 那麼對於已經安裝了中文應用組件的人而言, 這種狀況下您不需自己處理例外, 將它們交給預設的例外處理程序即可達到您的要求。 但是像上面的範例中, 我們的例外處理中, 除了顯示訊息之外, 有一個很重要的任務就是將運算結果清除為零(vFLOAT :=0.0), 換句話說, 我們應該可以將運算結果清除為零之後, 其餘的事情交給預設的例外處理程序。前面提到過, 任何例外只要經過例外處理程序的處理之後就會消失, 因此在例外經過我們區域性的例外處理(如上例的將運算結果清除為零)之後, 例外也消失了, 那麼該如何將此例外交給預設的處理程序呢? 其實答案很簡單, 我們只要在區域的處理例外之後, 重新發生一次例外即可, 語法如下: try <保護區塊> except <區域性的例外處理> raise; end; 請看下面這段程式碼範例: procedure TForm1.Button1Click(Sender: TObject); var vINT:Integer; vFLOAT:double; begin try try vINT:=StrToInt(Edit1.Text); vFLOAT:= 1/vINT; except vFLOAT:=0.0;{我們自己的例外處理} raise;{再發生一次同樣的例外,以交給預設的例外處理程序去處理} end; finally Label1.Caption:=FloatToStr(vFLOAT); end; end; 在區域的例外處理之後, 我們加了一行raise; 這會重新產生一個一模一樣的例外, 此例外會繼續往上傳, 換句話說, try…except…end之後仍然會有一個例外存在, 直到預設的例外處理程序處理為止。 產生一個自訂的例外 既然Delphi的例外處理機制是如此強大, 我們可以根據自己的邏輯產生特定的例外, 如此便可以利用Delphi的例外處理機制來處理自行定義的錯誤。 在Borland Delphi 2.x的範例程式中, Borland\Delphi 2.0\Demos\Doc\Filmanex\Fmxutils.pas包含了許多實用的檔案操作的函式, 其中有一個求檔案大小的函式 , 原始碼如下: function GetFileSize(const FileName: string): LongInt; var SearchRec: TSearchRec; begin if FindFirst(ExpandFileName(FileName), faAnyFile, SearchRec) = 0 then Result := SearchRec.Size else Result := -1; end; 它會先去搜尋這個檔案是否存在, 如果存在則會傳回這個檔案的大小, 否則傳回 -1的值代表錯誤。因此在使用這個函式時, 我們必須用傳統的if … then … else來判斷錯誤, 而無法享受到Delphi的例外處理機制所帶來的好處, 例如: procedure TForm1.Button1Click(Sender: TObject); var fsize:Integer; begin fsize:=GetFileSize(Edit1.Text); if fsize>0 then ShowMessage(Edit1.Text+'的檔案大小為:' +IntToStr(fsize)) else ShowMessage(Edit1.Text+'檔案不存在'); end; 但是如果我們將GetFileSize函式, 改寫成下面的GetFileSizeE, 則情況將會改觀: function GetFileSizeE(const FileName: string): LongInt; var SearchRec: TSearchRec; begin if FindFirst(ExpandFileName(FileName), faAnyFile, SearchRec) = 0 then Result := SearchRec.Size else {raise一個例外} raise EInOutError.Create('檔案'+FileName+'不存在'); end; GetFileSizeE的功能與GetFileSize一樣, 但是它會在找不到檔案時主動產生一個例外, 因此再使用這個函式時便可以享受到Delphi的例外處理機制所帶來的好處: procedure TForm1.Button2Click(Sender: TObject); var fsize:Integer; begin fsize:=GetFileSizeE(Edit1.Text); ShowMessage(Edit1.Text+'的檔案大小為:' +IntToStr(fsize)); end; 上面這段使用GetFileSizeE的範例, 如果檔案不存在時, GetFileSizeE會產生一個例外, 而使得顯示檔案大小的訊息不會顯示, 取而代之的是Delphi預設的例外處理程序所顯示的錯誤訊息'檔案…不存在'。 值得一提的是, GetFileSizeE所產生的例外是屬於EInOutError的類別, 這是因為我們在raise這個例外時, 是用EInOutError這個類別所建立起來的, 這樣做的目的是希望提供更多的資訊供爾後的例外處理程序使用。 有時候, 我們會希望產生一個自訂型態的例外, 因為這將有助於例外處理程序的判斷, 使我們更精準的處理錯誤。要產生一個自訂型態的例外您必須先定義這個例外的類別, 然後產生由這個所建立的例外。 再一次, 我們將修改Borland Delphi 2.x所附的範例Fmxutils.pas中的程序MoveFile, 讓它可以針對不同的錯誤狀況產生我們自訂的例外。 首先, 我們定義了下列例外類別: type EMoveErr = class(EInOutError); ESourceErr = class(EMoveErr); EFileNotExists = class(ESourceErr); ESourceReadOnly = class(ESourceErr); EDestinationErr = class(EMoveErr); EDestExists = class(EDestinationErr); EWritePathErr = class(EDestinationErr); 它們的繼承關係如下圖所示: 它們的意義則如下表所示: EMoveErr 移動檔案時發生錯誤 ESourceErr 錯誤發生在來源檔 EDestinationErr 錯誤發生在目的檔 EFileNotExists 來源檔不存在 ESourceReadOnly 來源檔是唯讀的(無法刪除) EDestExists 目的檔已經存在(不准覆寫) EWritePathErr 目的檔的路徑無法寫入 我們故意定義了關係稍微複雜的例外類別, 以便在後面的應用上更方便; 因為由上圖我們可以知道, 當發生EFileNotExists的例外時, 因為它是繼承自ESourceErr, 也等於發生了ESourceErr的例外, 同理, 當我們知道發生了ESourceErr的例外時, 也等於發生了EMoveErr的例外; 換句話說, 當來源檔案不存在的例外發生時, 便可確知錯誤發生在來源檔, 當確知錯誤發生在來源檔時, 便一定知道檔案的移動發生了錯誤… 整個的繼承關係正是在表達這個意義。 這在稍後的範例中可以得到證實。 定義完例外的別之後, 便可在適當的狀況下產生例外了, 下面是改寫自Delphi範例MoveFile的MoveFileE : procedure MoveFileE(const SourceFile, DestName: string); var Destination: string; DestDir:string; begin {定義來源檔的錯誤狀況} if not FileExists(SourceFile) then raise EFileNotExists.Create( Format('檔案 %s 不存在',[SourceFile]) ); if HasAttr(SourceFile, faReadOnly) then raise ESourceReadOnly.Create( Format('唯讀檔案 %s 無法移動',[SourceFile]) ); {定義目的檔的錯誤狀況} Destination := ExpandFileName(DestName); DestDir := ExtractFilePath(Destination); if FileExists(Destination) then raise EDestExists.Create( Format('目的檔案 %s 已經存在',[Destination]) ); if not DirWriteable(DestDir) then raise EWritePathErr.Create( Format('目錄 %s 無法寫入',[DestDir]) ); {如果來源檔與目的檔都正確, 開始 Move} if not RenameFile(SourceFile,Destination) then {先試著 Rename} begin {如果不行 Rename 就 Copy & Delete} try CopyFile(SourceFile, Destination); except {因為不知道是Source 還是 Destination 發生錯誤, 因此直接產生EMoveErr} raise EMoveErr.Create('無法移動檔案.'); end; try DeleteFile(SourceFile); except {確定錯誤發生在 Source 因此產生ESourceErr} raise ESourceErr.Create( Format('無法刪除檔案%s.',[SourceFile]) ); end; end; end; 注意到了嗎? 什麼狀況該產生什麼例外, 完全是由我們自己訂定: 1. 檢查SourceFile 是否存在, 如果不存在就產生EFileNotExists 的例外。 2. 檢查SourceFile 是否唯讀, 如果是, 則產生ESourceReadOnly 的例外。 3. 檢查目的檔Destination是否已存在, 如果是, 則產生EDestExists的例外。 4. 檢查目的路徑的讀寫權限, 如果無法寫入則產生EWritePathErr 的例外。 5. 接著試著RenameFile如果成功則結束, 否則繼續下面的步驟。 6. 試著 CopyFile(SourceFile, Destination) 並用例外處理機制來保護, 如果這個過程產生例外, 則重新產生EMoveErr的例外。 7. 試著 DeleteFile(SourceFile) 並用例外處理機制來保護, 如果這個過程產生例外, 則重新產生ESourceErr的例外。 現在讓我們簡單的來測試一下, 先寫個最簡單而且不作任何例外處理的程序如下: procedure TForm1.Button1Click(Sender: TObject); begin MoveFileE(Edit1.text,Edit2.text); end; 完成之後您可以故意假設一些狀況來作測試, 例如故意在Edit1.Text中輸入一個不存在的路徑, 或著故意在Edit2.Text指定到一個無法寫入的路徑等, 您可以發現, 測試過程中所有的例外都可以顯示出正確的訊息。 如果今天我們只關心到底是來源檔出了錯誤還是目的檔出了錯誤, 那麼您可以簡單的加上例外處理: procedure TForm1.Button2Click(Sender: TObject); begin try MoveFileE(Edit1.text,Edit2.text); except on ESourceErr do ShowMessage('發生 ESourceErr 的例外;來源檔錯誤'); on EDestinationErr do ShowMessage('發生 EDestinationErr 的例外;目的檔錯誤'); end; end; 現在您再重複地進行如同上面的測試, 結果發現, 無論是ESourceErr, EFileNotExists, ESourceReadOnly都同樣地可以被on ESourceErr do … 所控制, 而EDestinationErr, EDestExists, EWritePathErr都同樣地可以被on EDestinationErr do … 所控制, 為何可以這麼做呢? 答案就在它們的繼承關係。 因此, 如果您希望發生錯誤時, 可以更精準的控制例外, 那麼在定義例外時就應該多考慮一下, 尤其是自行設計元件時, 這樣的考量特別重要, 因為您有義務在發生錯誤時產生例外。 安靜的Exception -- Abort Delphi定義了一個特殊的例外EAbort , 這是一個安靜的例外, Delphi預設的例外處理程序不會對EAbort及繼承自它的例外, 用一個對話盒顯示它的訊息, 這在某些場合上是非常有用的。 例如, 假設Table1中有一個欄位名為'Name' 我們希望在使用者輸入的內容為空字串時就取消Post的動作, 那麼程式碼可以如下撰寫: procedure TForm1.Table1BeforePost(DataSet:TDataSet); begin if DataSet.FieldByName('Name')='' then raise EAbort.Create(''); end; 這個程序會因為例外而取消Post的動作,但是Delphi並不會顯示任何的訊息。 嗯! 看起來好像有點麻煩, 因此Delphi提供了一個更簡單的方法來達到這個目的, 就是Abort程序, 呼叫Abort程序會自動產生一個無聲的例外, 所以上面的範例可以改寫如下: procedure TForm1.Table1BeforePost(DataSet:TDataSet); begin if DataSet.FieldByName('Name')='' then Abort; end; 說到這裡, 您應該知道, 如果您想要自行定義一個安靜的例外只要繼承自EAbort即可, 例如: type EMySilentErr = class(EAbort); 巢狀的try…except與try…finally 當您想要使用巢狀的例外控制的語法時, 請記住以下幾個原則: O Try…except…end的語法結構中except…end所處理過的例外會被消滅, 但是您可以再度的產生一個例外, 或是因為處理不當而再度發生另一個例外時, 此例外會被傳遞到上一層。 O Try…finally…end的語法結構中並不會有任的例外被消滅。 O 所有的例外一但產生之後會一直往呼叫堆疊的上一層傳遞, 直到此例外被例外處理程序處理為止。換句話說, 在一層層的呼叫程序或是巢狀的程式區塊中, 一但例外產生了, 它就會一直往上(往外)傳遞, 直到被消滅為止。 相信這幾個原則, 讀者應該已經很清楚了, 現在讓我們直接看範例: procedure TForm1.btnProcClick(Sender: TObject); var FileList:TStrings; LogMsg:string; i:Integer; begin try FileList:=TStringList.Create; try FileList.LoadFromFile('Files.lst'); for i:=0 to FileList.count -1 do begin try ProcessFile(FileList[i]); except on E:Exception do begin LogMsg:='檔案錯誤:'+FileList[i]+ '訊息:'+E.Message; Memo1.Lines.Add(LogMsg); end; end; end; finally FileList.Free; end; except MessageBeep(-1); ShowMessage('檔案處理過程發生錯誤無法繼續!'); end; end; 為了說明方便, 我們在幾個重要的程式區塊劃上了方框並標上了編號。 O 首先讓我們先看一下B1.1.1與B1.1.2兩個區塊, 很明顯地, 只要B1.1.1發生了任的例外都會被B1.1.2處理(將錯誤訊息記錄到Memo1)並將此例外消滅掉, 只要B1.1.2不再產生例外, 則這兩個區塊並不會有任的例外向外傳遞。 O 接著, B1.1與B1.2是try…finally…end的結構, 因此不會有任何的例外被消滅, 換句話說, 只要B1.1有例外產生, 它會往傳遞到外面一層去。但無論是否有例外產生, B1.2一定會被執行到, 可以確定FileList會被釋放。 O 最後將重點放在B1與B2, 這是try…except…end的結構, 因此理論上, B1與B2並不會有例外向外傳遞, 因為只要B1中還有未被處理的例外都會被B2處理掉。 所以, 如果B1.1.1中的ProcessFile(…) 發生了例外, 因為它會立刻被B1.1.2所處理, 而會將錯誤訊息記錄到Memo1中, 但您不會收到任何顯示錯誤訊息的對話盒, 但是如果例外發生在B1.1的FileList. LoafFromFile(…) 因為例外尚未被消滅, 於是開始往外傳遞, 最後被B2所處理, 所以您會看見B2所顯示的訊息對話盒。 Exit,Break與Continue的影響 在try…finally…end的結構中, 如果try…finally之間有任何因為呼叫Exit, Break或Continue的程序而造成了跳離try…finally區塊, finally…end區塊仍然會被執行。例如: 01:procedure TForm1.Button1Click(Sender: TObject); 02:var i:Integer; 03:begin 04: for i:=1 to 5 do 05: begin 06: try 07: if i=3 then Break; 08: ShowMessage('Loop:'+IntToStr(i)); 09: finally 10: ShowMessage('finally block executed at Step:' 11: +IntToStr(i)); 12: end; 13: end; 14:end; 雖然當i=3時, 在行7會立刻跳離for迴圈, 而使得行8不會執行, 但是因為finally區塊的控制, 行10仍然會被執行, 所以整個看到的訊息依序為: Loop:1 finally block executed at Step:1 Loop:2 finally block executed at Step:2 finally block executed at Step:3 整合環境中的Exceptions 在Delphi的整合環境中, 即使您對例外已經做了控制, 但為何有時候仍然會出現Delphi顯示例外訊息的對話盒? 其實這是Delphi所提供的偵錯能力, 其目的是要告訴您更詳細的例外資訊。如果您不想看到這個額外的對話盒, 請依下列步驟執行: O 在Delphi 2.0整合開發環境的主選單中, 選擇Tools然後點選Options… 此時會出現一個參數設定的對話盒。 O 選擇References頁, 在Debugging的方框中, 將Break on Exception的核對方塊清除(不要打勾), 然後按OK。 Application的OnException事件 如果您不喜歡Delphi預設的例外處理程序, 有一個機會可以去改變它, 就是利用Application的OnException事件。 只有下列條件都符合時, OnException事件才會被觸發: 1. 您有指定Application的OnException事件。 2. 程式中發生了例外而且此例外尚未被處理。 3. 此例外不是EAbort類別。 4. 此例外不是繼承自EAbort。 範例: procedure TForm1.FormCreate(Sender: TObject); begin Log:=TStringList.Create;; Application.OnException:=ProcException; end; procedure TForm1.ProcException(Sender: TObject; E: Exception); begin ShowMessage(E.Message); Log.Add(E.Message); end; procedure TForm1.FormDestroy(Sender: TObject); begin Log.SaveToFile('ErrorLog.txt'); Log.free; end; 事實上, Delphi預設的處理例外程序就是是Application的HandleException方法(mothed), 這個方法在實作上會呼叫Application的另一個方法--ShowException, 來顯示例外訊息對話盒。 因此如果上面的例子中, 您只想將有關檔案IO的例外記錄下來, 而其他的例外則顯示例外訊息即可, 則程式碼可以改寫如下: procedure TForm1.ProcException(Sender: TObject; E: Exception); begin if E is EInOutError then Log.Add(E.Message) else Application.ShowException(E); end; 值得注意的是, 您不應該在Application的OnException事件中, 再產生例外, 因為這樣會造成呼叫上的遞回, 而無法預期執行的結果, 不信的話, 您可以試試看。 深入Exception Classes 您想知道Exceptions的宣告嗎? 在SysUtils中您可以得到答案: Exception = class(TObject) private FMessage: string; FHelpContext: Integer; public constructor Create(const Msg: string); constructor CreateFmt(const Msg: string; const Args: array of const); constructor CreateRes(Ident: Integer); constructor CreateResFmt(Ident: Integer; const Args: array of const); constructor CreateHelp(const Msg: string; AHelpContext: Integer); constructor CreateFmtHelp(const Msg: string; const Args: array of const; AHelpContext: Integer); constructor CreateResHelp(Ident: Integer; AHelpContext: Integer); constructor CreateResFmtHelp(Ident: Integer; const Args: array of const; AHelpContext: Integer); property HelpContext: Integer read FHelpContext write FHelpContext; property Message: string read FMessage write FMessage; end; 您可以發現, Exception的宣告中有好幾個建構者(constructors) , 換句話說, 您可以在raise例外時, 選擇一個最符合您需求的建構者, 此外Message與HelpContext都是可以讀寫的屬性, Message屬性存放這個例外的描述字串或只說是顯示訊息, 而HelpContext則提供了輔助說明的線上參考能力, 至於各建構者的差異, 請參考下表: 建構者 參數 說明 Create Msg 以訊息字串建立例外,例外的屬性Message會設定成Msg字串 範例: raise Exception.Create('Test Err'); CreateFmt Msg Args 以格式字串組合的訊息來建立例外, 參數Msg與Args的用法與format()函式相同 範例: raise Exception.CreateFmt('Error code:%d',[code]); CreateRes Ident 以訊息字串建立例外,但建立例外時的訊息字串,取自資源檔,由Ident指出資源代號 範例: raise Exception.CreateRes(2001); CreateResFmt Ident Args 以格式字串組合的訊息來建立例外,但組合訊息時的格式字串取自資源檔,由Ident指出資源代號 範例: raise Exception.Create(2001,[code]); CreateHelp Msg AhelpContext 以訊息字串與輔助說明的索引號碼來建立例外,由AhelpContext指出輔助說明的索引號碼 範例: raise Exception.CreateHelp('Some Error',101); CreateFmtHelp Msg Args AhelpContext 以格式字串組合的訊息與輔助說明的索引號碼來建立例外,由AhelpContext指出輔助說明的索引號碼 範例: raise Exception.CreateFmtHelp('Error code:%d',[code],101); CreateResHelp Ident AhelpContext 以訊息字串與輔助說明的索引號碼來建立例外,但建立例外時的訊息字串,取自資源檔,由Ident指出資源代號,由AhelpContext指出輔助說明的索引號碼 範例: raise Exception.CreateResHelp(2001,101) CreateResFmtHelp Ident Args AhelpContext 以格式字串組合的訊息與輔助說明的索引號碼來建立例外,但組合訊息時的格式字串取自資源檔,由Ident指出資源代號,由AhelpContext指出輔助說明的索引號碼 範例: raise Exception.CreateResFmtHelp(2001,[code],101) 您可以繼承任何一個例外的類別, 然後加以擴充, 以自訂例外的類別, 例如, SysUtils中例外EInOutError的宣告: EInOutError = class(Exception) public ErrorCode: Integer; end; EInOutError就是繼承自Exception, 並且多加了一個資料成員ErrorCode, 於是EInOutError除了擁有所有Exception的屬性外還多了一個ErrorCode可以用來記錄錯誤代碼, 事實上, Delphi的例外與一般物件在操作上是一樣的。 |