Effective Delphi
條款1:不管怎麼樣,請讓你的Project至少user一次SysUtils.pas單元
很多使用Delphi的人都對Delphi有著這樣一個抱怨:Delphi雖然開發效率高,但是其編譯出來的程式卻是太大。使用Delphi5建立一個Project然後直接編譯,程式的Size就已經達到了286KB,而如果把同樣的程式放到Delphi7下面編譯的話,那麼其Size更是達到了360KB。正是由於這點,所以為Delphi編譯產生的應用程式“減肥”便成為了幾乎所有Delphi社區的一個保留性話題。
其間,大多數人都是使用可執行檔壓縮公用程式(比如Aspack或者Upx等)來壓縮Delphi所產生的可執行檔以達到“減肥”的目的,但是也有一些人,他們使用一種更為極端,但是更有效方式來減少Delphi編譯產生的可執行檔的Size,那就是拋棄VCL所提供的編程架構,而直接使用WIN32 SDK加上Object Pascal所提供的物件導向來能來進行程式的撰寫。比如以下一段程式,使用Delphi5的編譯器進行編譯其大小隻有16KB,而寫一個基本的帶視窗的Window程式其大小也不會超過25KB(以下這段程式使用Delphi3編譯後會更小,其原因請見下述):
CODE:program SmallPro;
uses
Windows;
{$R *.RES}
begin
MessageBox(0, 'Hello World!', 'Information', MB_OK);
end.
[Copy to clipboard]
請大家注意,以上程式是在project檔案內直接編譯,所以沒有引用到其它的自訂單元。而所包含的Windows單元則只是為了調用MessageBox API函數而必須包含的。
這個編譯出來的程式實在是太小了,小到它足以對那些熟悉Windows SDK方式編程,而又使用Delphi作為開發工具的人產生一定的誘惑力(我自己就算一個:->)。不知道這種方式是否同樣也對你產生過誘惑力或者已經對你產生了誘惑力,如果是的話,那麼先請聽我一句忠告,“請為你的Project Uese上SysUtils.pas單元吧,否則你的程式將失去使用異常機制的能力,如果你不接這條忠告的話,你早晚會為你的行為任出代價。”
關於異常處理,各人的看法不同,有的人認為它是一種極美妙的錯誤處理方式:因為它能夠使程式碼中處理錯誤部分的代碼與實現邏輯部分的代碼分離,使程式的原始碼變得更優雅且撰寫起來更方便和易讀。而有人則認為使用異常機制來處理常式中的錯誤是不好的行為:因為一旦異常被觸發,並且你未對其加控制的話,那麼這個異常將導致應用程式終止,這種錯誤處理方式太過直接和粗魯。但是,不管怎樣,無庸置疑一點的是,你的程式碼可以不使用異常機制來處理錯誤,但是你卻無法預計在你的代碼當中所調用的各種庫函數或者類是否使用或者支援異常機制,所以為了保證你程式的魯棒性,即使你的代碼不使用異常機制,那麼你也應該在你代碼的關鍵位置,加入異常處理的代碼,以免你的代碼所調用的其它代碼或者作業系統拋出異常,導致程式意外的終止。下面便是一個簡單的小例了:
CODE:program SmallPro;
uses
Windows,
SysUtils;
{$R *.RES}
var
p: PChar;
begin
try
p := nil;
p^ := 'l';
except
MessageBox(0, 'Exception', 'Information', MB_OK);
end;
end.
[Copy to clipboard]
以上程式向地址空間0x00000000寫一個位元組的資料,在現在所有版本的Windows作業系統下面,這都將被系統視為非法操作,所以作業系統會拋出一個SHE異常,而Delphi的RTL系統會使你的程式能夠欄截住這個異常並加以處理,如果你的程式沒有處理這個異常的話,那麼Delphi的RTL會彈出一個顯示異常資訊的對話方塊,並在你按下對話方塊的“確定”按鈕後終止整個程式。我們上面的程式處理了這個異常,程式將在彈出MessageBox函數所顯示的對話方塊後繼續執行try…except.塊後面的代碼。
下面我們將上面的這個例子做一個很小的改動,將uses的SysUtils.pas單元去掉,然後再運行看看會出現什麼樣結果。
程式執行的結果和uses了SysUtils.pas單元的版本有著相當大的差異,程式只會顯示一個如所示的:Runtime Error的對話方塊,然後便終止運行了,我們的異常處理塊try..except則根本就沒有起到作用。
(圖1:執行階段錯誤)
經過以上的測試,我想你已經能夠明白,如果想讓你的使用Delphi編譯器所編譯出來的程式能夠支援異常機制的話,那麼你就必須去在你的項目當中至少的包含的一次SysUtils.pas單元。寫到此處,此條款應該可以說是功德圓滿,但是我想我還是有必要帶你簡單的瞭解一下Delphi的整個異常處理機制,以便你能夠對SysUtils.pas單元在整個Delphi異常機制中所佔的地位有一個進一步的認識,並能夠做到更好的使用它。
追根溯源,Delphi的編譯器其實會向C/C++編譯器一樣為你的程式在連結時插入一段啟動代碼來使作業系統能夠調用它,並啟動整個應用程式(這個不是C/C++的main函數,如果你對這方面感興趣的話,我建議你去讀Jeffry Richter所著的《Programming Applications for Microsoft Windows Fourth Edition》,這本書的第4章對Processes的講述中有相關的描述)。
對於以EXE形式存在的和以DLL形式存在的程式來說,Delphi為它們插入的啟動代碼的名稱是不一樣的,對於EXE型程式來說,Delphi會為你的程式插入其一個名稱為_InitExe的過程,而對於DLL型程式來說,Delphi編譯器會為你的程式插入一個名稱為_InitLib的過程,你可以從SysInit.pas單元的原始碼當中找到這兩個過程的定義和實現。說到這裡順便提一句,System.pas和SysInit.pas兩個Pascal單元是Delphi編譯器在編譯器時預設包含的兩個單元(你從來沒有見到過哪一個程式uses過這兩個單元吧)。而Delphi的每一個版本幾乎都會對這兩個單元進行擴充和修改,也正因為這個原因,所以在前面你看到的使用Delphi7編譯的那個小程式的Size要比Delphi5編譯出來的同樣程式大的多。
在_InitExe過程的內部會調用一個名稱為_StartExe的過程,而在這個_StartExe過程的內部中則會去調用在System.pas單元中定義的SetExceptionHandler函數來初始化整個Delphi的異常處理機制,在這個過程中設定的_ExceptionHandler過程則正是Delphi整個異常處理機制的核心處理過程。
在_ExceptionHandler會使用到System.pas單元中定義的一系列過程指標變數(比如ExceptProc,ExceptClsProc,ExceptObjProc等),這些過程指標變數都是Delphi整個異常機制當中必須的使用到的,而這些變數的初始化工作便是在SysUtils.pas單元中定義的InitExceptions單元中,InitExceptions變數會在SysUtils單元的initialization部分被調用,於是整個Delphi的異常處理機制便初始化完成。對於DLL型的程式,其異常處理過程的初始化部分與EXE型的程式一樣,所以在這裡就不再複述了。
好了在介紹了Delphi最核心的異常處理過程之後,我們再來介紹一下這些異常處理過程是如何被觸發的。
當是一個異常被觸發後,作業系統會最先攔截到這個異常。在作業系統攔截到這個異常後,它會馬上調用.KiUserExceptionDispatcher函數(注1),這個函數是的Windows作業系統自身使用的異常處理函數,而在KiUserExceptionDispathcher函數調用的過程中,它會通過某種回調機制,最終去調用我們上面提到過的_ExceptionHandler過程,展開異常並處理之,如果沒有找到如果被拋出異常所匹配的異常,那麼則調用在SysUtils.pas中被賦值的ExceptHandler過程指標變數,拋出一個出現異常資訊的話框,並在使用者按下確定按鈕之後終止程式。
這裡面值得一的是,如果ExceptHandler過程指標變數的值為Nil,那麼Delphi的RTL會去調用System.pas單元中定義的MapToRunError過程來做一個異常類型到執行階段錯誤碼的映射,並最終調用RunErrorAt過程在顯示執行階段錯誤對話方塊(1所示)終止整個程式的運行。
注1:我的作業系統是WIN2K所以KiUserExceptionDispatcher在ntdll當中,由於條件有限我沒有在WIN98下做過類似的調試,不知道在WIN98下面是否也使用類似的方式來處理異常。另由於KiUserExceptionDispatcher函數微軟未文檔化的一個函數,所以我在這裡不太方便對此函數的運作機制進行剖析(因為不同的作業系統中此函數的實現機制可能會不同),所以在這裡還請您見諒。