1. 程式人生 > >[轉]李戰大師-悟透delphi-第一章 delphi的原子世界

[轉]李戰大師-悟透delphi-第一章 delphi的原子世界


======================================================
注:本文原始碼點此下載
======================================================

yjmyzz:李戰大師的成名,並不是因為08年發表於園子裡的那篇"悟透javascript",而是多年前的這篇處女作"悟透delphi",原出處已經找不到了,近日重溫delphi研究如何開發原生win32中的activex控制元件時,無意又找到了這篇文章,想當年這篇文章在delphi程式設計群體中那是何等轟動,轉載於此,以示紀念。(delphi的出現,秒殺了vb/pb,vs的出現又秒殺了delphi,但是windows就其發展來看,不管如何發展,至少在今後相當長的時候間,也不可能徹底放棄win32的原生程式支援,所以存在即合理,delphi有某些領域仍是有用武之地的)

正文開始:

第一章delphi的原子世界

“天蒼蒼,野茫茫,風吹草低 見牛羊”在使用delphi開發應用軟體的過程中,我們就像草原上一群快樂牛羊,無憂無慮地享受著object pascal語言為我們帶來的溫暖陽光和各種vcl控制元件提供的豐富水草。擡頭望望無邊無際蔚藍的天空,低頭品嚐大地上茂密的青草,誰會去想天有多高?地有多大?陽光和水草又是從何而來?那是大師關心的事。而大師此時正坐在高高的山頂上,仰望宇宙星雲變換,凝視地上小蟲的爬行。驀然回頭,對我們這群吃草的牛羊點頭微笑。隨手扯起一根小草,輕輕地含在嘴裡,閉上眼睛細細品嚐。不知道這根青草在大師的嘴裡是什麼味道?只是,他的臉上一直帶著滿意的微笑。

第一節system

不經意,偶然打開了system.pas的原程式檔案,卻發現這裡竟是一個既熟悉又陌生的世界。在這裡有我們熟知的東東,如:tobject、tclass、guid、iunknown、idispatch ……但這些東西也是我們所陌生的。在茫茫程式設計生涯中,我們不斷地與這些東東打交道,都已經熟悉得宛如自己身體的一部分。但真想要去了解他們,也就人象想要了解自身一樣的茫然。

在system.pas單元的開頭,有這樣一段醒目的註釋文字:

{ predefined constants, types, procedures, }

{ and functions (such as true, integer, or }

{ writeln) do not have actual declarations.}

{ instead they are built into the compiler }

{ and are treated as if they were declared }

{ at the beginning of the system unit.}

這段話的意思是說:“這一單元包含預定義的常量、型別、過程和函式(諸如:ture、integer或writeln),它們並沒有實際的宣告,而是編譯器內建的,並在編譯的開始就被認為是已經宣告的定義”。

system單元不同於別的單元。你可以將classes.pas或windows.pas等其他delphi源程式檔案加入你的專案檔案中進行編譯,並在原始碼基礎上除錯這些單元。但你絕對無法將system.pas源程式檔案加入到你的專案檔案中編譯!delphi將報告“重複定義了system單元”的編譯錯誤。

任何delphi的目標程式中,都自動包含system單元中的程式碼,哪怕你的程式一句程式碼也沒寫。看看下面的程式:

program nothing;

begin

end.

這個程式用delphi 6編譯之後有8k,用delphi 5編譯之後有16k。而使用過c語言的朋友都知道,最簡單的c語言程式編譯之後是非常短小的,有的不到1k。但delphi不是的。

這個什麼也不做的程式怎麼會有8k或16k的長度呢?這是因為其含有system單元的程式碼。雖然這些程式碼沒有c或c++語言的啟動程式碼那樣短小精悍,但裡面卻包含支撐整座delphi大廈的基石,是很牢靠的。

在delphi6中,borland為了相容其在linux下的旗艦產品kylix,進一步精簡了system單元的基礎程式,將一部分與windows系統相關的內容移到了別的單元。所以,上面最簡單的程式經過delphi6編譯生成的目標程式就比delphi5生成的小的多。其實,delphi 6中的system.pas單元有一萬八千多行源程式,比delphi 5的多得多。這是因為在delphi6的那些支援kylix的單元中,有些程式碼同時寫了兩個版本,一個支援windows,一個支援linux,並在編譯巨集命令的控制下生成各自作業系統的目標程式。borland完成這些程式改寫之後,就有可能將delphi編寫的程式移植到kylix上。按照borland提供的某些原則編寫的delphi程式可以不用修改直接在kylix上編譯,並在linux系統上執行。這對需要進行跨平臺開發的程式設計師來說無疑是個福音。目前,在真編譯的可視開發工具中,delphi 6和kylix恐怕是唯一能實現跨平臺編譯功能的開發工具。

走馬觀花,瀏覽一下delphi的原始碼是值得的。因為,delphi的原始碼中蘊藏著豐富的營養,那都是大師們的傑作。如果,我們開發的應用應用程式是一棵開花的樹,那麼,請在我們擁有這份花滿枝丫的浪漫時,請不要忘了深埋在土壤裡的那一藤樹根。沒有樹根提供營養,就沒有爛漫的花枝。要知道,世界上任何一棵樹的樹根總比其樹冠更多,更茂盛,儘管人們看不到深埋在地下的樹根。

但瀏覽delphi的源程式也是很費精力的。雖然,大師們寫的程式大都風格一流,易於閱讀和理解,但程式碼實在太多。閱讀system.pas單元就更不容易,其中的大量程式甚至是用匯編語言編寫的,這對有些朋友來說無異於天書。我們無意逐一去解讀其中的奧祕,這可能會耗用我們九九八十一個不眠之夜。但我們總能學到一些程式設計風格,瞭解其中的一些內容,並能悟得一些道理,而這可能會讓我們受益終身。

當然,我無意將delphi的原始碼神化為聖典。因為,那也畢竟不是天書,也是人編寫的,也能抓到其中的幾隻臭蟲。但我們自己又怎樣呢?

第二節tobject

tobject是什麼?

tobject在delphi中就是與生俱來的東西,沒有什麼好問的。

不知道tobject是什麼,照樣可以編寫出很好的delphi程式。我們可以小心苛護自己的delphi程式,“朝朝勤拂拭,莫讓惹塵埃”,我們的程式也能照樣歡快地奔跑。世界上有很多的東西都是我們不知道的,我們一樣也生活得很好。

但世上總有些人就是喜歡去學習和探索那些不知道的東西,最終他們知道的東西總比別人多些,成為了智者。我想,在程式設計中也是這樣,如果經過我們不斷地學習和探索,將不知道的東西變成我們知道的東西,我們也會逐漸成為程式設計中的智者。相信總有一天能進入“本來無一物,何處惹塵埃”的境界。

tobject是system單元中定義的第一個類。由此可見它在delphi中的重要性。tobject的定義是這樣的:

tobject = class

constructor create;

procedure free;

class function initinstance(instance: pointer): tobject;

procedure cleanupinstance;

function classtype: tclass;

class function classname: shortstring;

class function classnameis(const name: string): boolean;

class function classparent: tclass;

class function classinfo: pointer;

class function instancesize: longint;

class function inheritsfrom(aclass: tclass): boolean;

class function methodaddress(const name: shortstring): pointer;

class function methodname(address: pointer): shortstring;

function fieldaddress(const name: shortstring): pointer;

function getinterface(const iid: tguid; out obj): boolean;

class function getinterfaceentry(const iid: tguid): pinterfaceentry;

class function getinterfacetable: pinterfacetable;

function safecallexception(exceptobject: tobject;

exceptaddr: pointer): hresult; virtual;

procedure afterconstruction; virtual;

procedure beforedestruction; virtual;

procedure dispatch(var message); virtual;

procedure defaulthandler(var message); virtual;

class function newinstance: tobject; virtual;

procedure freeinstance; virtual;

destructor destroy; virtual;

end;

tobject還真有不少東東。

注意,tobject是class型別。

說到這裡,也許有人要問這需要特別注意嗎?

在此,我只是想提醒大家不要忘了,在object pascal語言中還有一種以object保留字定義的物件型別。這種資料板塊套上過程作為方法的老古董,同樣實現了面向物件的各種特徵,只不過它並非現代delphi大廈的奠基石。有點象是歷史文化遺產,屬於傳統文化系列。但瞭解歷史可以更深刻地理解現在並展望未來。現在,class 系列的物件類才是delphi的基礎,它和物件的介面技術一起,支撐起整個delphi大廈。我們所講的物件幾乎都是class系列的。所以如果沒有特別指明,“物件”一詞都指class型別的物件。

我們都知道,在delphi中tobject是所有class系列物件的基本類。也就是說,在delphi中,tobject是萬物之源。不管你自定義的類是否指明瞭所繼承的父類,一定都是tobject的子孫,一樣具有tobject定義的所有特性。

那麼,一個物件到底是什麼?

物件就是一個帶柄的南瓜。南瓜柄就是物件的指標,南瓜就是物件的資料體。確切地說,delphi中的物件是一個指標,這個指標指向該物件在記憶體中所佔據的一塊空間。

雖然,物件是一個指標,可是我們引用物件的成員時卻不能寫成這樣的程式碼:

myobject^.getname;

而只能寫成:

myobject.getname;

這是object pascal語言擴充的語法,是由編譯器支援的。使用c++ builder的朋友就很清楚物件與指標的關係,因為在c++ builder的vcl物件都是通過指標引用的。

為什麼說物件是一個指標呢?我們可以試著用sizeof函式獲取物件的大小,例如計算sizeof(myobject)的值。結果是4位元組,這就就是一個32位指標的大小,只是南瓜柄的大小。而物件的真正大小應該用myobject.instancesize獲得,這才是南瓜應有的份量。廣義的說,我們常用的“控制代碼”概念,英文叫handle,也是一個物件指標,因為它後面也連著一個別的什麼瓜。

既然delphi物件是指向一塊記憶體空間的指標,那麼,代表物件的這快記憶體空間又有怎樣的資料結構呢?就把南瓜切開來看看囉。

我們將物件指標指向的記憶體空間稱為物件空間。物件空間的頭4個位元組是指向該物件直屬類的虛方法地址表(vmt – vritual method table)。接下來的空間就是儲存物件本身成員資料的空間,並按從該物件最原始祖先類的資料成員到該物件具體類的資料成員的總順序,和每一級類中定義資料成員的排列順序儲存。

每一個類都有對應的一張vmt,類的vmt儲存從該類的原始祖先類派生到該類的所有類的虛方法的過程地址。類的虛方法,就是用保留字vritual宣告的方法。虛方法是實現物件多型性的基本機制。雖然,用保留字dynamic宣告的動態方法也可實現物件的多型性。但這樣的方法不儲存在vmt中。用保留字dynamic宣告的動態方法只是object pascal語言提供的另一種可節約類儲存空間的多型實現機制,但卻是以犧牲呼叫速度為代價的。

即使,我們自己並未定義任何類的虛方法,但該類的物件仍然存在指向虛方法地址表的指標,只是地址項的長度為零。可是,在tobject中定義的那些虛方法,如destroy、freeinstance等等,又儲存在什麼地方呢?原來,他們的方法地址儲存在相對vmt指標負方向偏移的空間中。在vmt的負方向偏移有76個位元組的資料資訊,它們是物件類的基本資料結構。而vmt是儲存我們自己為類定義的虛方法地址的地方,它只是類資料結的構擴充套件部分。vmt前的76個位元組的資料結構是delphi內定的,與編譯器相關的,並且在將來的delphi版本中有可能被改變。

下面的物件和類的結構草圖展示了物件和類之間的一些關係。

tobject中定義的有關類資訊或物件執行時刻資訊的函式和過程,一般都與類的資料結構相關。

在delphi中我們用tobject、tcomponent等等識別符號表示類,它們在delphi的內部實現為各自的vmt資料。而用class of保留字定義的類的型別,實際就是指向相關vmt資料的指標。

對我們的應用程式來說,類的資料是靜態的資料。當編譯器編譯完成我們的應用程式之後,這些資料資訊已經確定並已初始化。我們編寫的程式語句可訪問類資料中的相關資訊,獲得諸如物件的尺寸、類名或執行時刻的屬性資料等等資訊,或者呼叫虛方法以及讀取方法的名稱與地址等等操作。

當一個物件產生時,系統會為該物件分配一塊記憶體空間,並將該物件與相關的類聯絡起來。於是,在為物件分配的資料空間中的頭4個位元組,就成為指向類vmt資料的指標。

我們再來看看物件是怎樣誕生和滅亡的。我們都知道,用下面的語句可以構造一個最簡單物件:

anobject := tobject.create;

編譯器將其編譯實現為,用tobject對應的類資料資訊為依據,呼叫tobject的create建構函式。而tobject的create建構函式呼叫了系統的classcreate過程。系統的classcreate過程又通過呼叫tobject類的虛方法newinstance。呼叫tobject的newinstance方法的目的是要建立物件的例項空間。tobjec類的newinstance方法將根據編譯器在類資訊資料中初始化的物件例項尺寸(instancesize),呼叫getmem過程為該物件分配記憶體。然後呼叫tobject類initinstance方法將分配的空間初始化。initinstance方法首先將物件空間的頭4個位元組初始化為指向物件類的vmt的指標,然後將其餘的空間清零。建立物件例項最後,還呼叫了一個虛方法afterconstruction。最後,將物件例項資料的地址指標儲存到anobject變數中,這樣,anobject物件就誕生了。

同樣,用下面的語句可以消滅一個物件:

anobject.destroy;

tobject的解構函式destroy被宣告為虛方法,這可以讓某些有個性的物件選擇自己的死亡方法。destory方法首先呼叫了beforedestruction虛方法,然後呼叫系統的classdestroy過程。classdestory過程又通過呼叫物件的freeinstance虛方法。由freeinstance方法呼叫freemem過程釋放物件的記憶體空間。就這樣,一個物件就在系統中消失。

物件的析構過程比物件的構造過程簡單,就好像生命的誕生是一個漫長的孕育過程,而死亡卻相對的短暫,這似乎是一種必然的規律。

在物件的構造和析構過程中,呼叫了newinstance和freeinstance兩個虛擬函式,來建立和釋放物件例項的記憶體空間。之所以將這兩個函式宣告為虛擬函式,是為了能讓使用者在編寫需要使用者自己管理記憶體的特殊物件類時(如在一些特殊的工業控制程式中),有擴充套件的空間。

而將afterconstruction和beforedestruction宣告為虛擬函式,也是為了將來派生的類在產生物件之後,有機會讓新誕生的物件呼吸第一口新鮮空氣,而在物件消亡之前可以允許物件交待最後的遺言,這都是合情合理的事。例如,我們熟悉的tform物件和tdatamodule物件的oncreate事件和ondestroy事件,就是分別在這兩個過載的虛擬函式中觸發的。

此外,tobjec還提供了一個free方法。它不是虛方法,它是為了在搞不清物件指標是否為空(nil)的情況下,也能安全釋放物件而專門提供的。當然,搞不清物件指標是否是否為空,本身就有程式邏輯不清晰的問題。不過,任何人都不是完美的,都可能犯錯,使用free能避免偶然的錯誤也是件好事。然而,編寫正確的程式不能一味依靠這樣的解決方法,還是應該以保證程式的邏輯正確性為程式設計的第一目標。

有興趣的朋友可以讀一讀system單元的原始碼,其中,大量的程式碼是用匯編語言書寫的。細心的朋友可以發現,tobject的建構函式create和解構函式destory竟然沒有寫任何程式碼。其實,在除錯狀態下通過debug的cpu視窗,可清楚地反映出create和destory的彙編程式碼。我想,可能是因為締造delphi的大師門不想將過多複雜的東西提供給使用者。他們希望使用者在簡單的概念上編寫應用程式,將複雜的工作隱藏在系統的內部由他們來承擔。所以,在編寫system.pas單元時特別將這兩個函式的程式碼去掉,讓使用者認為tobject是萬物之源,使用者派生的類完全從虛無中開始,這本身並沒有錯。

第三節tclass

在system.pas單元中,tclass是這樣定義的:

tclass = class of tobject;

它的意思是說,tclass是tobject的類。因為tobject本身就是一個類,所以tclass就是所謂的類的類。

從概念上說,tclass是類的型別,即,類之類。但是,我們知道delphi的一個類,代表著一項vmt資料。因此,類之類可以認為是為vmt資料項定義的型別,其實,它就是一個指向vmt資料的指標型別!

在以前傳統的c++語言中,是不能定義類的型別的。物件一旦編譯就固定下來,類的結構資訊已經轉化為絕對的機器程式碼,在記憶體中將不存在完整的類資訊。一些較高階的面嚮物件語言才可支援對類資訊的動態訪問和呼叫,但往往需要一套複雜的內部解釋機制和較多的系統資源。而delphi的object pascal語言吸收了一些高階面嚮物件語言的優秀特徵,又保留可將程式直接編譯成機器程式碼的傳統優點,比較完美地解決了高階功能與程式效率的問題。

正是由於delphi在應用程式中保留了完整的類資訊,才能提供諸如as和is等在執行時刻轉換和判別的高階面向物件功能,而類的vmt資料在其中起了關鍵性的核心作用。有興趣的朋友可以讀一讀system單元的asclass和isclass兩個彙編過程,他們是as和is操作符的實現程式碼,這樣可以加深對類和vmt資料的理解。

有了類的型別,就可以將類作為變數來使用。可以將類的變數理解為一種特殊的物件,你可以象訪問物件那樣訪問類變數的方法。例如:我們來看看下面的程式片段:

type

tsampleclass = class of tsampleobject;

tsampleobject = class( tobject )

public

constructor create;

destructor destroy; override;

class function getsampleobjectcount:integer;

procedure getobjectindex:integer;

end;

var

asampleclass : tsampleclass;

aclass : tclass;

在這段程式碼中,我們定義了一個類tsampleobject及其相關的類型別tsampleclass,還包括兩個類變數asampleclass和aclass。此外,我們還為tsampleobject類定義了建構函式、解構函式、一個類方法getsampleobjectcount和一個物件方法getobjectindex。

首先,我們來理解一下類變數asampleclass和aclass的含義。

顯然,你可以將tsampleobject和tobject當作常量值,並可將它們賦值給aclass變數,就好象將123常量值賦值給整數變數i一樣。所以,類型別、類和類變數的關係就是型別、常量和變數的關係,只不過是在類的這個層次上而不是物件層次上的關係。當然,直接將tobject賦值給asampleclass是不合法的,因為asampleclass是tobject派生類tsampleobject的類變數,而tobject並不包含與tsampleclass型別相容的所有定義。相反,將tsampleobject賦值給aclass變數卻是合法的,因為tsampleobject是tobject的派生類,是和tclass型別相容的。這與物件變數的賦值和型別匹配關係完全相似。

然後,我們再來看看什麼是類方法。

所謂類方法,就是指在類的層次上呼叫的方法,如上面所定義的getsampleobjectcount方法,它是用保留字class宣告的方法。類方法是不同於在物件層次上呼叫的物件方法的,物件方法已經為我們所熟悉,而類方法總是在訪問和控制所有類物件的共同特性和集中管理物件這一個層次上使用的。

在tobject的定義中,我們可以發現大量的類方法,如classname、classinfo和newinstance等等。其中,newinstance還被定義為virtual的,即虛的類方法。這意味作你可以在派生的子類中重新編寫newinstance的實現方法,以便用特殊的方式構造該類的物件例項。

在類方法中你也可使用self這一識別符號,不過其所代表的含義與物件方法中的self是不同的。類方法中的self表示的是自身的類,即指向vmt的指標,而物件方法中的self表示的是物件本身,即指向物件資料空間的指標。雖然,類方法只能在類層次上使用,但你仍可通過一個物件去呼叫類方法。例如,可以通過語句aobject.classname呼叫物件tobject的類方法classname,因為物件指標所指向的物件資料空間中的頭4個位元組又是指向類vmt的指標。相反,你不可能在類層次上呼叫物件方法,象tobject.free的語句一定是非法的。

值得注意的是,建構函式是類方法,而解構函式是物件方法!

什麼?建構函式是類方法,解構函式是物件方法!有沒有搞錯?

你看看,當你建立物件時分明使用的是類似於下面的語句:

aobject := tobject.create;

分明是呼叫類tobject的create方法。而刪除物件時卻用的下面的語句:

aobject.destroy;

難道不是嗎?tobject是類,而aobject是物件。

原因很簡單,在構造物件之前,物件還不存在,只存在類,建立物件只能用類方法。相反,刪除物件一定是刪除已經存在的物件,是物件被釋放,而不是類被釋放。

最後,順便討論一下虛建構函式的問題。

在傳統的c++語言中,可以實現虛解構函式,但實現虛建構函式卻是一個難題。因為,在傳統的c++語言中,沒有類的型別。全域性物件的例項是在編譯時就存在於全域性資料空間中,函式的區域性物件也是編譯時就在堆疊空間中對映的例項。即使是動態建立的物件,也是用new操作符按固定的類結構在堆空間中分配的例項,而建構函式只是一個對已產生的物件例項進行初始化的物件方法而已。傳統c++語言沒有真正的類方法,即使可以定義所謂靜態的基於類的方法,其最終也被實現為一種特殊的全域性函式。更不用說虛擬的類方法,虛方法只能針對具體的物件例項有效。因此,傳統的c++語言認為,在具體的物件例項產生之前,卻要根據即將產生的物件構造物件本身,這是不可能的。的確不可能,因為這會在邏輯上產生自相矛盾的悖論!

然而,正是由於在delphi中有動態的類的型別資訊,有真正虛擬的類方法,以及建構函式是基於類實現的等等這些關鍵概念,才可實現虛擬的建構函式。物件是由類產生的,物件就好象成長中的嬰兒,而類就是它的母親,嬰兒自己的確不知道自己將來會成為什麼樣的人,可是母親們卻用各自的教育方法培養出不同的人,道理是相通的。

都知道強大的vcl是delphi得以成功的基礎之一,而所有vcl的鼻祖是tcomponent類。在tcomponent類的定義中,建構函式create被定義為虛擬的。這能使不同型別的控制元件實現各自的構造方法,這就是tclass創造的類之類概念的偉大,也是delphi的偉大。

第四節 運用tobject的方法

我們先來看看tobject的各個方法都是些什麼東東。簡要列示如下:

constructor create;

tobject類的建構函式,用於建立物件。

procedure free;

安全釋放物件資料空間。

class function initinstance(instance: pointer): tobject;

初始化新建物件的資料空間。

procedure cleanupinstance;

在物件被釋放前清除物件的資料空間。

function classtype: tclass;

獲得物件直屬的類。

class function classname: shortstring;

獲得物件直屬類的名稱。

class function classnameis(const name: string): boolean;

判斷物件直屬類的名稱是否是指定的名稱。

class function classparent: tclass;

獲得物件或類的上一代類,即父類。

class function classinfo: pointer;

獲得物件類的執行時型別資訊(rtti),一般用於tpersistent類。

class function instancesize: longint;

獲得物件例項的大小。

class function inheritsfrom(aclass: tclass): boolean;

判斷物件或類是否是從指定的類派生的。

class function methodaddress(const name: shortstring): pointer;

獲得物件或類指定方法名稱的呼叫地址。該方法必須是published的。

class function methodname(address: pointer): shortstring;

獲得物件或類指定方法地址的方法名稱。該方法必須是published的。

function fieldaddress(const name: shortstring): pointer;

獲得物件指定屬性名稱的訪問地址指標。

function getinterface(const iid: tguid; out obj): boolean;

獲得物件支援指定介面標識的介面。

class function getinterfaceentry(const iid: tguid): pinterfaceentry;

獲得物件或類指定介面標識的介面項。

class function getinterfacetable: pinterfacetable;

獲得物件或類支援的所有介面項的資訊表。

function safecallexception(exceptobject: tobject;exceptaddr: pointer): hresult; virtual;

支援介面物件safecall呼叫異常處理虛方法,常被介面物件過載。

procedure afterconstruction; virtual;

物件建立後首先被呼叫的虛方法,供派生類物件過載以初始化新物件。

procedure beforedestruction; virtual;

物件釋放前最後被呼叫的虛方法,供派生類物件過載以清理物件資料。

procedure dispatch(var message); virtual;

物件的訊息處理方法,支援windows等訊息處理。

procedure defaulthandler(var message); virtual;

預設的訊息處理方法。

class function newinstance: tobject; virtual;

分配物件例項空間的虛方法。

procedure freeinstance; virtual;

釋放物件例項空間的虛方法。

destructor destroy; virtual;

物件的析構虛方法,用於消滅物件。

這些方法在delphi的幫助文件中都有描述。有些方法已在前面簡單介紹過。由於tobject的方法也比較多,一時也講不完。就挑一兩個說說其用法吧,其他的在後面用到時再細細道來也不遲。

就說說methodaddress和fieldaddress物件方法吧。

在使用delphi開發程式的過程中,我們經常會與vcl的屬性和事件打交道。我們新增元件到設計視窗中,設定元件的相關屬性,為元件的各種事件編制處理事件的方法。然後,輕輕鬆鬆地編譯,程式就誕生了,一切都是視覺化的。

我們知道,在設計時delphi將元件的資料成員(包括欄位、屬性和事件)等資訊儲存在*.dfm檔案中,並將其作為資源資料編譯到最終的執行程式中。delphi的編譯過程同時也將源程式中類的結構資訊和程式碼也編譯到執行程式中,這些資訊在執行時可以由程式訪問的。

delphi的程式在執行時建立的form或datamodule等物件時,首先建立該物件。接著,從相應的資源資料中讀取設計時保留的資料成員資訊,並使用fieldaddress方法獲取資料成員的訪問地址。然後,用設計時定義的值初始化該資料成員。如果是事件,則再呼叫methodaddress獲取事件處理程式的呼叫地址,並初始化該事件。這樣就完成了設計時的資料程式碼關係到執行時的資料程式碼關係的對映,有點兒象動態連線過程。

原來,元件的某些的資料成員和方法是可以用名稱去訪問的,就是使用fieldaddress和methodaddress方法。其實,這種功能是在最基礎的tobject中就支援的。當然,只有定義為published訪問級別的資料成員和方法才可以使用名稱去訪問,而定義為private、protected和public訪問級別的除外。

注意,只有型別是類或介面的資料成員才可定義為published的訪問級別,方法都是可以定義為published的。對於從tpersistent繼承的那些物件類,如果沒有特別宣告資料成員和方法的訪問級別的,則預設是published的。例如,tform類是tpersistent派生下來的,一個典型的form類的定義中,由delphi的ide自動維護和生成的那些資料成員和方法,預設都是published的。因為,tpersistent類使用了特殊的{$m+}編譯選項。

知道這層內幕之後,我們也可以自己使用這些方法來實現一些有意義的功能。

第五節 物件的訊息處理機制

tobject的定義中,有兩個方法值得我們注意,就是:

procedure dispatch(var message); virtual;

procedure defaulthandler(var message); virtual;

這兩個方法是delphi的vcl強大的訊息處理機制的基礎,windows的各種訊息最終都是通過這兩個個方法處理掉的。

在講述這一問題之前,有必要先說明一下什麼是訊息。

從廣義上將,訊息就是資訊的傳遞,一個物件將自己知道的事情通知其他物件。每個物件可以根據得到的訊息做出相應的反應。訊息在現實世界中普遍存在,故事、新聞、命令、報告等等,當然也包括流言蜚語。在程式中表現為資料訪問、過程呼叫、方法呼叫、事件觸發和通訊協議等等。

而我們今天討論的訊息是狹義的訊息,這種訊息就是物件間的一種通訊協議。這種訊息溝通機制的特點是,相關物件之間不會象變數訪問和方法呼叫那樣是固定的耦合關係,而是非常自由和鬆散的關係。採用這種訊息機制,物件之間是的通訊方式是統一的,而訊息的內容是多種多樣的,一組通訊的物件之間可以約定自己的訊息格式和含義。雖然,一個物件可以和將訊息傳送給任何物件,也可以接收任何物件發來的訊息,但物件一般只處理和傳送自己關心的訊息。

在windows中的視窗、任務和程序等物件間的資訊溝通,都普遍採用這種訊息機制。實際上,訊息機制是windows的基礎之一。而delphi物件的訊息處理機制一開始就是為了支援windows訊息而設計的,特別是用於視窗類的控制元件(即從twincontrol繼承的控制元件)。但這種訊息機制已經能夠讓所有的tobject物件採用這種方式通訊。如,我們熟悉的tlabel雖然不是一個視窗控制元件,但仍然能收到windows發來的訊息。當然,windows是不會給一個tlabel傳送訊息的,那是delphi幫的忙。而tobject的dispatch方法在這一個過程中起了關鍵性作用。

我們知道,delphi將windows的訊息描述為是一個聯合結構,也叫變體結構。訊息結構的第一個成員是一個四位元組的整數,是區分訊息類別的標識。其餘的資料成員是根據訊息類別的不同而有不同的定義。正是因為其餘的成員是可以自由定義的,才使得訊息處理機制有良好的擴充套件性。要知道,windows有幾千種不同型別的訊息,delphi也自己擴充套件了若干種訊息。隨著軟體版本的發展,訊息的種類還會不斷增加。

關心某種訊息的物件類會為指定的訊息定義一個訊息處理方法,訊息處理方法是用保留字message來宣告的。例如:

tmouseobject = class(tobject)

public

procedure wmmousemove(var msg:tmessage); message wm_mousemove;

procedure wmlbuttondown(var msg:tmessage); message wm_lbuttondown;

end;

delphi的編譯器將根據message保留字識別訊息處理方法,並生成一個訊息標識到該物件方法的對映表,連線到最終的執行程式中。事實上在delphi的內部,訊息處理方法是用dynamic方法的機制實現的。前面我們說過,dynamic型別的方法是delphi的另一種虛方法,是可以過載以實現物件類的多型性。事實上,dynamic方法就是根據方法的序號找到呼叫地址的,這與根據訊息id找到各自的訊息處理地址是沒有什麼本質區別的。因此,訊息處理方法是可以由子類過載的,這可以讓繼承的物件實現自己的訊息處理。不過,這種過載的語義與dynamic的過載有些不同。訊息處理方法是按訊息標識來過載的,即按message保留字後面的值。雖然,子類的訊息處理方法的名稱可以不同,只要訊息標識相同即可實現過載。例如:

tnewmouseobject = class(tmouseobject)

public

procedure mousemove(var msg:tmessage); message wm_mousemove;

procedure mousedown(var msg:tmessage); message wm_lbuttondown;

end;

其中,mousemove方法過載了父類的wmmousemove方法,而mousedown過載了wmlbuttondown方法。當然,你也可以完全按dynamic的語義來定義過載:

tnewmouseobject = class(tmouseobject)

public

procedure wmmousemove(var msg:tmessage); override;

procedure wmlbuttondown(var msg:tmessage); override;

end;

雖然,這沒有任何錯誤,但我們很少這樣寫。這裡只是要向大家說明message與dynamic的本質相同之處,以加深印象。

根據訊息id找到處理該訊息的方法地址,就是所謂的“訊息分發”,或者叫“訊息派遣”,英文叫“dispatch”。所以,tobject的dispatch方法正是這個意思!只要你將訊息傳遞給tobject的dispatch方法,它將會正確地找到該訊息的處理方法並交給其處理。如果,dispatch方法找不到處理該訊息的任何方法,就會呼叫defaulthandler虛方法。雖然,tobject的defaulthandler沒有做任何事,但子類可以過載它以便自己處理漏網的訊息。

dispatch方法有一個唯一的引數message,它是var的變數引數。這意味著可以通過message引數返回一些有用的資訊給呼叫者,實現資訊的雙向溝通。每一個訊息處理方法都有一個唯一的引數,雖然引數型別是不一樣的,但必須是var的變數引數。

值得注意的是,dispatch的message引數是沒有型別的!

那麼,是不是任何型別的變數都可以傳遞給物件的dispatch方法呢?

答案是肯定的!

你可以將integer、double、boolean、string、variant、tobject、tclass……傳遞給一個物件的dispatch方法,編譯都不會出錯。只不過delphi可能找不到這些東東對應的訊息處理方法,即使碰巧找到,可能也是牛頭不對馬嘴,甚至產生執行錯誤。因為,dispatch總是將message引數的頭4個位元組作為訊息id到dynamic方法表中尋找呼叫地址的。

為什麼delphi要這樣定義dispatch方法呢?

因為,訊息型別是多種多樣的,訊息的大小和內容也是各不相同,所以只能將dispatch方法的message引數定義為無型別的。當然,delphi要求message引數的頭4個位元組必須是訊息的標識,但編譯器並不檢查這一要求。因為,這可能會擴充object pascal的語法定義,有些得不償失,也許將來會解決這個問題。

通常,訊息被定義為一個結構。這個結構的頭4個位元組被定義為訊息標識,其餘部分可以自由定義,大小隨意。windows的訊息結構大小是固定的,但delphi可以允許定義任意大小的訊息結構。雖然非windows要求的固定大小訊息結構可能無法用於windows系統的訊息傳遞,但對於我們在程式模組間定義自己的訊息應用來說,卻是非常方便的。

第六節 天蒼蒼,野茫茫

說了半天,我們已經瞭解到delphi原子世界的一個大概,也對delphi的最基礎的一些東西有了一定的輪廓。這對於今後的學習和開發來說是非常有好處的,因為,我們畢竟知道了一些東西在內部是怎樣實現的。

當然,還有許多東西我們還沒有討論,如類結構中的那些資料又指向什麼地方?執行時刻資訊(rtti)又是什麼結構?要把這些東東都討論完,我們還需要進行更多的探索,恐怕最後的結果可以寫一本厚厚的書。在今後的學習中,我們會再涉及到其中的內容。

真希望有一天我們能夠徹底瞭解delphi原子世界的所有奧祕。但這似乎是不可能的,因為delphi還在不斷髮展,新的技術會不斷的引入。因此,我們不追求最終的結果,探索的過程往往比最終的結果更幸福。

只要不斷的努力,相信有一天,我們能上升到另一更高的思想境界。那時,我們將更加充實,世界在我們眼裡將變得更美麗。雖然,天還是那樣的藍,大地還是那樣的綠,但我們的心情又會怎樣呢?

“天蒼蒼,野茫茫,風吹草低 見牛羊”

……


======================================================
在最後,我邀請大家參加新浪APP,就是新浪免費送大家的一個空間,支援PHP+MySql,免費二級域名,免費域名繫結 這個是我邀請的地址,您通過這個連結註冊即為我的好友,並獲贈雲豆500個,價值5元哦!短網址是http://t.cn/SXOiLh我建立的小站每天訪客已經達到2000+了,每天掛廣告賺50+元哦,呵呵,飯錢不愁了,\(^o^)/