在應用程序開發中如何檢測、處理程序的運行錯誤是一個很重要的問題。在 Delphi 的集成開發環境( IDE )中提供了一個完善的內置調試器,可以幫助你發現大部分程序錯誤。但並不是所有的錯誤都可以被發現,而且當程序涉及到與外設的數據交換或操作外設,如要求用戶輸入、讀寫磁盤等時,錯誤的發生是程序無法控制的,如輸入非法字符、磁盤不能讀寫等。這些情況不僅會導致應用程序異常中止而且可能引起系統的崩潰。針對這些問題,Delphi同時提供了一套強大的異常處理機制。巧妙地利用它,可以使你的程序更為強健,使用更為友好。
雖然Delphi為應用程序提供了一套缺省的自動異常處理機制,即當前模塊發生錯誤後退出當前模塊並給出錯誤信息,而並不立即引起應用程序的中止。但當應用程序執行的過程性很強時,僅僅利用這種方法是不夠的,而且很容易導致程序執行的不可預測性。
12.1 Delphi異常處理機制與異常類
Delphi異常處理機制建立在保護塊(Protected Blocks)的概念上。所謂保護塊是用保留字try和end封裝的一段代碼。保護塊的作用是當應用程序發生錯誤時自動創建一個相應的異常類(Exception)。程序可以捕獲並處理這個異常類,以確保程序的正常結束以及資源的釋放和數據不受破壞。如果程序不進行處理,則系統會自動提供一個消息框。
異常類是Delphi異常處理機制的核心,也是Delphi異常處理的主要特色。下面我們對異常類的概念和體系進行詳細的介紹。
Delphi提供的所有異常類都是類Exception的子類。用戶也可以從Exception派生一個自定義的異常類。
Exception類的定義如下,對於不常用的成員沒有列出。
{SysUtils 單元中}
Exception = class(TObject)
private
FMessage: PString;
FHelpContext: Longint;
function GetMessage: String;
procedure SetMessage(const Value: String);
public
constructor Create(const Msg: String);
constructor CreateFmt(const Msg: String; const Args: array of const);. . .
destructor Destroy; override;
property HelpContext: Longint
property Message: String;
property MessagePtr: PString;
end;
Exception的一系列構造函數中最重要的參數是顯示的錯誤信息。而數據成員中最重要的也是可被引用的消息字符串(message,messagePtr)。 這些信息分別對自定義一個異常類和處理一個異常類有重要作用。
Delphi提供了一個很龐大的異常類體系,這些異常類幾乎涉及到編程的各個方面。從大的方面我們可以把異常類分為運行時間庫異常、對象異常、部件異常三類。下面我們分別進行介紹。
12.1.1 運行時間庫異常類(RTL Exception)
運行時間庫異常可以分為七類,它們都定義在SysUtils庫單元中。
12.1.1.1 I/O異常
I/O異常類EInOutError是在程序運行中試圖對文件或外設進行操作失敗後產生的,它從Exception派生後增加了一個公有數據成員ErrorCode,用於保存所發生錯誤的代碼。這一成員可用於在發生I/O異常後針對不同情況采取不同的對策。
當設置編譯指示{$I- } 時,不產生I/O異常類而是把錯誤代碼返回到預定義變量IOResult中。
12.1.1.2 堆異常
堆異常是在動態內存分配中產生的,包括兩個類EOutOfMemory和EInvalidPointer。
表12.1 堆異常類及其產生原因
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
異常類 引發原因
─────────────────────────────────
EOutOfMemory 沒有足夠的空間用於滿足所要求的內存分配
EInvalidPointer 非法指針。一般是由於程序試圖去釋放一個業已釋放的指針而引起的
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.1.1.3 整數異常
整數異常都是從一個EIntError類派生的,但程序運行中引發的總是它的子類:EDivByZero,ERangeError,EIntOverFlow。
表12.2 整數異常及其產生原因
━━━━━━━━━━━━━━━━━━━━━
異常類 引發原因
─────────────────────
EDivByZero 試圖被零除
ERangeError 整數表達式越界
EIntOverFlow 整數操作溢出
━━━━━━━━━━━━━━━━━━━━━━
ERangeError當一個整數表達式的值超過為一個特定整數類型分配的范圍時引發。比如下面一段代碼將引發一個ERangeError異常。
var
SmallNumber: ShortInt;
X , Y: Integer;
begin
X := 100;
Y := 75;
SmallNumber := X * Y;
end;
特定整數類型包括ShortInt、Byte以及與整數兼容的枚舉類型、布爾類型等。例如:
type
THazard = ( Safety , Marginal , Critical , Catastrophic );
var
Haz: THazard;
Item: Integer;
begin
Item:= 4;
Haz:= THazard ( Item );
end;
由於枚舉數越界而引發一個ERangeError異常。
數組元素越界也會引發一個ERangeError異常,如:
var
Values: array[1..10] of Integer;
i: Integer;
begin
for i := 1 to 11 do
Values[i] := i;
end;
ERangeError異常只有當類型檢查打開時才會引發。這可以在代碼中包含{$R+} 編譯指示或設置IDE Option|Project的Range_Checking Option選擇框。
EIntOverFlow異常類在Integer、Word、Longint三種整數類型越界時引發。如:
var
I : Integer;
a,b,c : Word;
begin
a := 10;
b := 20;
c := 1;
for I := 0 to 100 do
begin
c := a*b*c;
end;
end;
引發一個EIntOverFlow異常。
EIntOverFlow異常類只有在編譯選擇框Option|Project|Over_Flow_Check Option選中時才產生。當關閉溢出檢查,則溢出後變量保留該類整數的最大范圍值。
整數類型的范圍如下表。 表12.3 整數類型的范圍
━━━━━━━━━━━━━━━━━━━━━━━━━━━
類型 范圍 格式
───────────────────────────
Shortint -128 .. 127 有符號8位
Integer -32768 .. 32767 有符號16位
Longint -2147483648 .. 2147483647 有符號32位
Byte 0 .. 255 無符號8位
Word 0 .. 65535 無符號16位
━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.1.1.4 浮點異常
浮點異常是在進行實數操作時產生的,它們都從一個EMathError類派生,但與整數異常相同,程序運行中引發的總是它的子類EInvalidOp、EZeroDivide、EOverFlow、EUnderFlow。
表12.4 浮點異常類及其引發原因
━━━━━━━━━━━━━━━━━━━━━━━━
異常類 引發原因
────────────────────────
EInvalidOp 處理器碰到一個未定義的指令
EZeroDivide 試圖被零除
EOverFlow 浮點上溢
EUnderFlow 浮點下溢
━━━━━━━━━━━━━━━━━━━━━━━━
EInvalidOp最常見的引發原因是沒有協處理器的機器遇到一個協處理器指令。由於在缺省情況下Delphi總是把浮點運算編譯為協處理器指令,因而在386以下微機上常常會碰到這個錯誤。此時只需要在單元的接口部分設置全局編譯指示{$N-},選擇利用運行時間庫進行浮點運算,問題就可以解決了。
各種類型的浮點數(Real、Single、Double、Extended)越界引起同樣的溢出異常。這同整數異常類是不同的。
12.1.1.5 類型匹配異常
類型匹配異常EInvalidCast當試圖用As 操作符把一個對象與另一類對象匹配失敗後引發。
12.1.1.6 類型轉換異常
類型轉換異常EConvertError當試圖用轉換函數把數據從一種形式轉換為另一種形式時引發,特別是當把一個字符串轉換為數值時引發。下面程序中的兩條執行語句都將引發一個EConvertError異常。
var
rl : Real;
int: Integer;
begin
rl := StrToFloat(' $140.48');
int := StrToInt(' 1,402 ');
end;
要注意並不是所有的類型轉換函數都會引發EConvertError異常。比如函數Val當它無法完成字符串到數值的轉換時只把錯誤代碼返回。利用這一點我們在(6.2)節中實現了輸入的類型和范圍檢查。
12.1.1.7 硬件異常
硬件異常發生的情況有兩種:或者是處理器檢測到一個它不能處理的錯誤,或者是程序產生一個中斷試圖中止程序的執行。硬件異常不能編譯進動態鏈接庫(DLLs)中,而只能在標准的應用中使用。
硬件異常都是EProcessor異常類的子類。但運行時間並不會引發一個EProcessor 異常。 表12.5 硬件異常類及其產生原因
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
異常類 引發原因
─────────────────────────────────
Efault 基本異常類。是其它異常類的父類
EGPFault 一般保護錯。通常由一個未 初始化的指針或對象引起
EStackFault 非法訪問處理器的棧段
EPageFault Windows內存管理器不能正確使用交換文件
EInvalidOpCode 處理器碰到一個未定義的指令。這通常意味著處理器
試圖去操作非法數據或未初始化的內存
EBreakPoint 應用程序產生一個斷點中斷
ESingleStep 應用程序產生一個單步中斷
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EFault、EGPFault 往往意味著致命的錯誤。而EBreakPoint、ESingleStep被Delphi IDE的內置調試器處理。事實上前邊的五種硬件異常的響應和處理對開發者來說都是十分棘手的問題。
12.1.2 對象異常類
所謂對象異常是指非部件的對象引發的異常。Delphi定義的對象異常包括流異常、打印異常、圖形異常、字符串鏈表異常等。
12.1.2.1 流異常類
流異常類包括EStreamError、EFCreateError、 EFOpenError、EFilerError、EReadError、EWriteError、EClassNotFound。它們的結構關系如下:
EStreamError
|---------- EFCreateError
|---------- EFOpenError
|---------- EFilerError
|--------- EReadError
|--------- EWriteError
|--------- EClassNotFound
圖12.1 流異常結構圖
流異常在Classes庫單元中定義。
流異常引發的原因如表12.6。
表12.6 流異常類及其產生原因
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
異常類 引發原因
─────────────────────────────────
EStreamError 利用LoadFromStream方法讀一個流發生錯誤
EFCreateError 創建文件時發生錯誤
EFOpenError 打開文件時發生錯誤
EFilerError 試圖再次登錄一個存在的對象
EReadError ReadBuffer方法不能讀取特定數目的字節
EWriteError WriteBuffer方法不能寫特定數目的字節
EClassNotFound 窗口上的部件被從窗口的類型定義中刪除
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.1.2.2 打印異常類
打印異常類EPrinter當打印發生錯誤時引發。它在printers庫單元中定義。例如你的應用程序試圖向一個不存在的打印機打印或由於某種原因打印工作無法送到打印機時,就會產生一個打印異常。
12.1.2.3 圖形異常類
圖形異常類定義在Graphic 庫單元中,包括EInvalidGraphic和EInvalidGraphicOperation兩類。
EInvalidGraphic當應用程序試圖從一個並不包含合法的位圖、圖標、元文件或用戶自定義圖形類型的文件中裝入圖形時引發。例如下面的代碼:
Image1.Picture.LoadFromFile('Readme.txt');
由於Readme.txt並不包含一個合法的圖形,因而將引發一個EInvalidGraphic異常。
EInvalidGraphicOperation當試圖對一個圖形進行非法操作時引發。例如試圖改變一個圖標的大小。
var
AnIcon: TIcon;
begin
AnIcon := TIcon.Create;
AnIcon.LoadFromFile('C:\Windows\DIRECTRY.ICO');
AnIcon.Width := 100; { 引發一個圖形異常 }
...
12.1.2.4 字符串鏈表異常
字符串鏈表異常EStringListError、EListError在用戶對字符串鏈表進行非法操作時引發。由於許多部件(如TListBox,TMemo,TTabSet,…)都有一個TStrings類的重要屬性,因而字符串鏈表異常在部件操作編程中非常有用。
EStringListError異常一般在字符串鏈表越界時產生。例如對如下初始化的列表框:
ListBox1.Items.Add('First item');
ListBox1.Items.Add('Second item');
ListBox1.Items.Add('Third item');
則以下操作都會引起EStringListError異常:
ListBox1.Item[3] := ' Not Exist';
str := ListBox1.Item [3];
EListError異常一般在如下兩種情況下引發:
1.當字符串鏈表的Duplicates屬性設置為dupError時,應用程序試圖加入一個重復的字符串;
2.試圖往一個排序的字符串鏈表中插入一個字符串。
12.1.3 部件異常類
12.1.3.1 通用部件異常類
通用部件異常類常用的有三個:EInvalidOperation、EComponentError、EOutOfResource。其中EInvalidOperation、EOutOfResource在Controls單元中定義;EComponentError在Classes單元中定義。
1.非法操作異常 EInvalidOperation
EInvalidOperation 引發的原因可能有:
● 應用程序試圖對一個Parent屬性為nil的部件進行一些需要Windows句柄的操作
● 試圖對一個窗口進行拖放操作
● 操作違反了部件屬性間內置的相互關系等
例如,ScrollBar、Gauge等部件要求Max屬性大於等於Min屬性,因而下面的語句:
ScrollBar1.Max := ScrollBar1.Min-1;
將引發一個EInvalidOperation異常。
2.部件異常EComponentError
引發該異常的原因可能有:
● 在Register過程之外試圖登錄一個部件(常用於自定義部件開發中)
● 應用程序在運行中改變了一個部件的名稱並使該部件與另一個部件重名
● 一個部件的名稱改變為一個Object Pascal非法的標識符
● 動態生成一個部件與已存在的另一部件重名
3.資源耗盡異常EOutOfResource
當應用程序試圖創建一個Windows句柄而Windows 卻沒有多余的句柄分配時引發該異常。
12.1.3.2 專用部件異常類
許多部件都定義了相應的部件異常類。但並不是有關部件的任何錯誤都會引發相應的異常類。許多情況下它們將引發一個運行時間異常或對象異常。
下面列出幾個典型的部件異常類。
1.EMenuError
非法的菜單操作,例如試圖刪除一個不存在的菜單項。這一異常類在Menus庫單元中定義。
2.EInvalidGridOpertion
非法的網格操作,比如試圖引用一個不存在的網格單元。這一異常類在Grids庫單元中定義。
3.EDDEError
DDE異常。比如應用程序找不到特定的服務器或會話,或者一個聯接意外中止。這一異常類在DDEMan庫單元中定義。
4.EDatabaseError,EReportError
數據庫異常(EDatabaseError)和報表異常(EReportError) 在進行數據庫和報表操作出現錯誤時引發。有關數據庫的問題請讀者參閱本書第二編。
12.1.4 小結
在這一節中重點介紹了Delphi提供的異常類體系。我們力求給讀者一個清晰、全面的印象,使讀者能在自己的程序開發中實際使用它們。為便於理解我們也提供了一些簡單的說明性示例。雖然在具體的使用中讀者還可能會碰到許多問題,但意識到應該用異常類來增強程序的健壯性卻是程序設計水平走上新台階的標志。
12.2 異常保護
確保回收分配的資源是程序健壯性的一個關鍵。但缺省情況下異常發生時程序會在出錯點自動退出當前模塊,因此需要一種特殊的機制來確保即使在異常發生的情況下釋放資源的語句仍能被執行。而Delphi的異常處理正提供了這種機制。
12.2.1 需要保護的資源
一般說來需要保護的資源包括:
● 文件
● 內存
● Windows資源
● 對象
比如下面一段程序就會造成1K內存資源的丟失。
var
APointer : Pointer ;
AInt , ADiv: Integer ;
begin
ADiv := 0;
GetMem ( APointer , 1024 );
AInt := 10 div ADiv ;
FreeMem ( Apointer , 1024 );
end;
由於程序從異常發生點退出從而FreeMem永遠沒有執行的機會。
12.2.2 產生一個資源保護塊 Delphi提供了一個保留字finally,用於實現資源的保護:
{分配資源}
try
{資源使用情況}
finally
{釋放資源}
end;
try…finally…end就形成了一個資源保護塊。finally後面的語句是在任何情況下,不論程序是否發生異常,都會執行的。
對於(12.2.1)中的例子如下代碼即可確保所分配內存資源的釋放:
var
APointer : Pointer ;
AInt , ADiv : Integer;
begin
ADiv := 0;
GetMem ( APointer , 1024 );
try
AInt := 10 div ADiv ;
finally
FreeMem ( Apointer , 1024 );
end;
end;
下面的例子摘自(6.4)節,是在文件拷貝中實現文件資源的保護:
procedure CopyFile(const FileName, DestName: TFileName);
var
CopyBuffer: Pointer;
TimeStamp, BytesCopIEd: Longint;
Source, Dest: Integer;
Destination: TFileName;
const
ChunkSize: Longint = 8192;
begin
Destination := ExpandFileName(DestName);
if HasAttr(Destination, faDirectory) then
Destination := Destination + '\' + ExtractFileName(FileName);
TimeStamp := FileAge(FileName);
GetMem(CopyBuffer, ChunkSize);
try
Source := FileOpen(FileName, fmShareDenyWrite);
if Source < 0 then
raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));
try
Dest := FileCreate(Destination);
if Dest < 0 then
raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));
try
repeat
BytesCopIEd := FileRead(Source, CopyBuffer^, ChunkSize);
if BytesCopIEd > 0 then
FileWrite(Dest, CopyBuffer^, BytesCopIEd);
until BytesCopIEd < ChunkSize;
finally
FileClose(Dest);
end;
finally
FileClose(Source);
end;
finally
FreeMem(CopyBuffer, ChunkSize);
end;
end;
程序的具體解釋見 (6.4)節。
在異常保護的情況下,當異常發生時,系統會自動彈出一個消息框用於顯示異常的消息。退出當前模塊後異常類自動清除。
12.3 異常響應
異常響應為開發者提供了一個按自己的需要進行異常處理的機制。try …except …end形成了一個異常響應保護塊。與finally不同的是:正常情況下except 後面的語句並不被執行,而當異常發生時程序自動跳到except,進入異常響應處理模塊。當異常被響應後異常類自動清除。
下面的例子表示了文件打開、刪除過程中發生異常時的處理情況:
uses Dialogs;
var
F: Textfile;
begin
OpenDialog1.Title := 'Delete File';
if OpenDialog1.Execute then
begin
AssignFile(F, OpenDialog1.FileName);
try
Reset(F);
if MessageDlg('Erase ' +OpenDialog1.FileName + '?',
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
System.CloseFile(F);
Erase(F);
end;
except
on EInOutError do
MessageDlg('File I/O error.', mtError, [mbOk], 0);
on EAccessDenIEd do
MessageDlg('File Access denIEd.', mtError, [mbOk], 0);
end;
end;
end.
保留字on…do用於判斷異常類型。必須注意的是:except後面的語句必須包含在某一個on…do模塊中,而不能單獨存在。這又是同finally不同的一個地方。
12.3.1 使用異常實例
上面所使用的異常響應方法可總結為如下的形式:
on ExceptionType do
{響應某一類的異常}
這種方法唯一使用的信息是異常的類型。一般情況下這已能滿足我們的需要。但我們卻無法獲取異常實例中包含的信息,比如異常消息、錯誤代碼等。假設我們需要對它們進行處理,那麼就必須使用異常實例。
為了使用異常實例,需要為特定響應模塊提供一個臨時變量來保存它:
on EInstance : ExceptionType do …
在當前響應模塊中我們可以象使用一個普通對象那樣來引用它的數據成員。但在當前響應模塊之外不被承認。
下面的代碼用於獲取異常消息並按自己的方式顯示它:
{窗口中包括一個ScrollBar部件,一個Button部件}
procedure TErrorForm.Button1Click(Sender: TObject);
begin
try
ScrollBar1.Max := ScrollBar1.Min-1;
except
on E: EInvalidOperation do
MessageDlg('Ignoring Exception:'+E.Message,
mtInformation,[mbOK],0);
end;
end;
12.3.2 提供缺省響應
在異常響應模塊中,一般我們只對希望響應的特定異常進行處理。如果一個異常發生而響應模塊並沒有包含對它的處理代碼,則退出當前響應模塊,異常類仍被保留。
為了保證任何異常發生後都能在當前響應模塊中被清除,可以定義缺省響應:
try
{程序正常功能}
except
on ESomething do
{響應特定異常}
else
{提供缺省響應}
end;
由於else可以響應任何異常,包括我們一無所知的異常,因此在缺省響應中最好只包括諸如顯示一個消息框之類的處理,而不要改變程序的運行狀態或數據。
12.3.3 響應一族異常
諸如
on ExceptionType do
的異常響應語句不僅可響應本類異常,而且可以響應子類異常。對於象EIntError、EMathError等系統不會引發的異常,它們將只響應其子類異常。而對於象
on Exception do
這樣的語句將會對任何異常進行響應。
下面一段代碼對整數越界異常進行單獨處理,而對其它整數異常進行統一處理:
try
{整數運算}
except
on ERangeError do
{越界處理}
on EIntError do
{其它整數異常處理}
end;
由於異常在處理後即被清除,因而上面的代碼可保證不會使ERangeError異常被多次處理。假如顛倒兩條響應語句的順序,則ERangeError異常響應將永遠沒有被執行的機會。
由於異常在處理後即被清除,因而當希望對異常進行多次處理時就需要使用保留字raise來重引發一個當前異常。
下面的代碼同時使用了異常響應和異常保護。異常響應用於設置變量的值,異常保護用於釋放資源。當異常響應結束時利用raise重引發一個當前異常。
var
APointer: Pointer ;
AInt , ADiv: Integer;
begin
ADiv := 0;
GetMem ( APointer , 1024 );
try
try
AInt := 10 div ADiv ;
except
on EDivByZero do
begin
AInt := 0 ;
raise;
end;
end;
finally
FreeMem ( APointer , 1024 );
end;
end;
上面一段代碼體現了異常處理的嵌套。異常保護、異常響應可以單獨嵌套也可以如上例所示的那樣相互嵌套。
12.3.5 自定義異常類的應用 利用Delphi的異常類機制我們可以定義自己的異常類來處理程序執行中的異常情況。同標准異常不同的是:這種異常情況並不是相對於系統的正常運行,而是應用程序的預設定狀態。比如輸入一個非法的口令、輸入數據值超出設定范圍、計算結果偏離預計值等等。