Delphi物件模型(Part II) (轉)

gugu99發表於2007-10-14
Delphi物件模型(Part II) (轉)[@more@]

 

 

 

模型 (PART II) :namespace prefix = o ns = "urn:schemas--com::office" />

 

 

Delphi對於物件導向的支援豐富而且強大。除了傳統的類和物件,Delphi還提供了介面,異常處理,多執行緒程式設計等特性。這一章節深入講解了Delphi的物件模型。讀者應當對標準的Pascal比較熟悉,並且對有關物件導向程式設計的基本法則有一定了解。

(本文的英文原文將Delphi與 Pascal統一表述為Delphi,可能有概念不清之嫌疑。但在大多數情況下,相信讀者能夠根據上下文來判定文中所述之Delphi的具體含義——譯者注。)

 

物件(Object)

 

物件是類的一個動態的例項。這個動態例項包含了該類及其祖先類的所有欄位。物件還包含一個隱含的欄位用來儲存物件所屬類的一個類引用。

物件總是從堆中分配到,因此物件引用實際上是指向該物件的一個指標。設計人員負有在合適的時間建立和釋放物件的責任。為了建立一個物件,我們使用類引用並構造器方法,如下例所示:

Obj := TSomeClass.Create;


大多數的構造器命名為Create,但這只是一個約定,並不是Delphi所一定要求的。有時你會發現其他名稱的構造器,特別是在Delphi還不支援方法的過載之前定義的一些陳舊的類。為了最大限度的與C++Builder保持相容(因為C++Builder不允許自定義構造器名稱),最好仍舊使用Create,過載原先的構造器方法。

要刪除程式中不再使用的一個物件,呼叫Free方法。為了確保即使在有異常觸發的情況下,物件也能被正確釋放,使用 try-finally 異常處理。如下例所示:

Obj := TSomeOtherClass.Create;


try


  Obj.DoSomethingThatMightRaiseAnException;


  Obj.DoSomethingElse;


finally


  Obj.Free;


end;


 


釋放一個全域性的變數時,假如總是在釋放物件後即將該變數置為nil,那麼便不會留下一個包含無效指標的變數。釋放物件之前而將物件置為nil一定得小心謹慎。如果構造器或者構造器中呼叫的一個方法對該變數有一個引用,那麼你最好將該變數置為nil以防可能的隱患。一個簡單的方法是呼叫FreeAndNill過程(在SysUtils中宣告)。

GlobalVar := TFruitWigglies.Create;


try


  GlobalVar.EatEmUp;


finally


  FreeAndNil(GlobalVar);


end;


每一個物件都包含它所有欄位一個單獨的副本。欄位不能被多個物件所共享。如果確實需要共享一個欄位變數,那麼在單元層次上定義這個變數或者使用間接方法:在物件中使用指標或者物件引用來訪問公共資料。

 

繼承(Inheritance)

一個類可以繼承自另一個類。新派生的類繼承了基類中所有的欄位,方法以及屬性。Delphi只支援單一繼承,因此派生類只有一個基類。而基類也可以有自己的基類,如此迴圈不斷,一個類便繼承了所有祖先類的欄位,屬性和方法。類還可以實現任意多的介面。類似於,但C++不同的是,所有Delphi的類都繼承自同一個根類,那就是TObject。如果不顯式的指明基類,Delphi自動將TObject作為該類的基類。

提示
類最直接的父類稱為基類,這在類的宣告中可以體現出來。類的祖先類可以是類的基類,也可以是一直到TObject的繼承鏈中的其他祖先類。因而,在例子2-1中,類TCertificateOfDeposit只有一個基類叫TSavingsAccount;而它的祖先類分別是TObject,TAccount以及TSavingsAccount。

TObject類宣告瞭一些方法以及一個特殊的,隱藏的欄位專門用來存放對該物件所屬類的引用。這個隱藏的欄位指向類的虛擬方法表(VMT)。每一個類都有唯一的一個VMT並且所有該類的物件共用這個類的VMT。

可以將一個物件引用賦值給一個相同物件型別的,或者該類的任何一個祖先類的變數。換句話說,物件引用在宣告時候的型別不一定要和實際的物件型別相同,反過來賦值——將一個基類的物件引用賦值給派生類的變數——是不允許的,因為物件可能會是不同的型別。

Delphi保留了Pascal的強型別校驗特點,因此根據一個物件引用宣告時的型別對其進行檢查。這樣,要求所有的方法必須是類宣告的一部分,並且編譯器對和過程的變數也進行常規檢查。編譯器並不都將某個方法的呼叫繫結到特定的實現上。因為假如是一個虛方法,那麼只有到執行時間時,才可以根據物件的真正的型別來決定哪個方法被呼叫。本章“方法”一節中詳細說明了這個問題。

使用Is運算子來測試物件所屬的真正的類。當此類引用與物件的類相同或者此類引用是該物件類的一個祖先類時,返回True。當物件引用為nil或者不是該類,則返回False。

if Account is TCheckingAccount then ... // tests the class of Account if Account is TObject then ... // True when Account is not nil

可以使用型別轉換以獲得另一個型別的物件引用。型別轉換並不改變物件;它只是給你一個新的物件引用。通常可以使用as運算子進行型別轉換。as運算子自動檢查物件型別並且當物件的類並不是目標類的子類時將引發一個執行期錯誤。(SysUtils單元中將該執行期錯誤對映到EInvalidCast 異常中。)

另一種轉換物件引用的方法是使用目標類的名稱,類似函式呼叫。這種轉換不會進行型別檢查,因此只當你確信時才這麼做。如例子2-3所示:

2-3:使用靜態的型別轉換

var


  Account: TAccount;


  Checking: TCheckingAccount;


begin


  Account  := Checking;  //允許


  Checking := Account;  // 編譯錯誤


  Checking := Account as TCheckingAccount; //沒問題


  Account as TForm;   // 觸發一個執行期錯誤


  Checking := TCheckingAccount(Account);  //可用,但不推薦


  if Account is TCheckingAccount then  //更好的


  Checking := TCheckingAccount(Account)


  else


  Checking := nil;


欄位(Field)

欄位是物件內部的變數。一個類可以宣告任意多的欄位,並且每一個物件都有自己的一個對自己類以及所有祖先類的所有欄位的一個副本。或者說,欄位可以稱為一個資料成員,一個例項化的變數,或者一個特性。Delphi沒有提供類變數,類例項變數,靜態資料成員或者等同的東西(即在同一類的所有物件中共享的變數)。但是你可以使用單元層次上的變數來達到類似的效果。

欄位可以為任意型別除非是釋出的(published)。在釋出的宣告部分中的欄位必須要有執行時間型別資訊。詳見第三章內容。

在Delphi中,新建立一個物件時,該物件的所有的欄位被置空,也就是說,所有指標被初始化為nil,字串以及動態陣列的內容為空,數字值為0,布林型別的值為False,並且可變型別Variant的值被賦值為Unassigned。

派生的類可以定義與祖先類中同名的欄位。派生類的這個欄位隱藏了祖先類中相同名稱的欄位。派生類中的方法引用的是派生類中的該欄位,而祖先類的方法引用的是祖先類中的該欄位。

方法(Method)

方法是在類中實現的函式或者過程。C++中方法被稱為“成員函式”。方法與普通的過程和函式的區別是,在方法中有一個隱含的引數稱為Self,用來指向呼叫該方法的物件本身。這裡的Self與C++和Java中的相類似。呼叫一個方法與呼叫一個普通的函式或過程類似,但得將方法的名稱跟在物件引用之後,如:

Object.Method(Argument);


類方法(Class method)基於類及其祖先類。在類方法中,Self是對類的引用而不是對物件的引用。C++中類方法稱為“靜態成員函式”。

你可以呼叫在物件的類中以及祖先類裡宣告的物件方法。假如祖先類和派生類中定義了相同名稱的方法,Delphi將呼叫最外層派生的那個方法。如例2-4所示:

2-4:繫結靜態方法

type


  TAccount = class


  public


  procedure Withdraw(Amount: Currency);


  end;


  TSavingsAccount = class(TAccount)


  public


  procedure Withdraw(Amount: Currency);


  end;


var


  Savings: TSavingsAccount;


  Account: TAccount;


begin


  ...


  Savings.Withdraw(1000.00);  //呼叫TSavingsAccount.Withdraw


  Account.Withdraw(1000.00);  //呼叫TAccount.Withdraw


普通方法被稱為靜態方法的原因是編譯器直接將該呼叫和方法的實現繫結在一起。換句話說,靜態方法是靜態繫結的。在C++中稱普通方法被稱為“普通成員函式”,在Java中稱為“最終方法(final method)”。多數Delphi程式設計師不願使用靜態方法這個術語,而將之簡化稱為方法或者非虛擬方法。

虛方法是在執行期間而非編譯期間被繫結的一類方法。在編譯期間,Delphi根據物件引用的型別來決定可以呼叫的方法。與編譯期間直接指定一個特定的方法的實現不同的是,編譯器根據物件的實際型別存放一個間接的對方法的引用。執行期間,程式在類的執行期表(特別是VMT)中查詢方法,然後呼叫實際的型別的方法。物件的真正的類必須是在編譯期中宣告的類,或者它的一個派生的類——這一點不成問題,因為VMT提供了指向正確的方法的指標。

要宣告一個虛方法,可以在基類中使用vritual指示符,然後使用overr指示符以在派生的類中提供該方法的新的定義。與Java不同的是,Delphi中方法在預設情況下是靜態的,因此你必須使用virtual指示符來宣告一個虛方法。與C++不同的是,Delphi中要在派生類中覆蓋一個虛方法必須使用override指示符。

2-5 使用虛方法。

2-5 繫結虛方法

type


  TAccount = class


  public


  procedure Withdraw(Amount: Currency); virtual;


  end;


  TSavingsAccount = class(TAccount)


  public


  procedure Withdraw(Amount: Currency); override;


  end;


var


  Savings: TSavingsAccount;


  Account: TAccount;


begin


  ...


  Savings.Withdraw(1000.00);  // 呼叫TSavingsAccount.Withdraw


  Account := Savings;


  Account.Withdraw(1000.00);  // 呼叫TSavingsAccount.Withdraw


除了vritual指示符,你還可以使用dynamic指示符。兩者語義相同的,但實現不同。在VMT中查詢一個虛方法很快,因為編譯器在VMT中建了。而查詢一個動態方法慢一些。呼叫一個動態方法虛要在動態方法表(DMT)中進行線性查詢。在祖先類中查詢直到遇到TObject或者該方法被找到為止。在某些場合,動態方法佔用比虛方法更少的記憶體。除非要寫一個VCL的替代物,否則你應當使用虛方法而不是動態方法。參見第三章以詳細瞭解有關內容。

虛方法和動態方法可以在宣告時使用abstract指示符,這樣該類就不必給出對該方法的定義,但在派生的類中必須覆蓋(override)該方法。C++中抽象方法的術語稱為“純虛方法”。當你呼叫一個包含有抽象方法的類的建構函式時, Delphi將給出編譯警告,提示你可能有個錯誤。可能你要建立的是覆蓋(override)並且實現了該抽象方法的派生類的一個例項。定義了一個或者多個抽象方法的類通常稱為抽象類,儘管有些人認定該術語只適用於只定義了抽象方法的那些類。

提示:
當你構建一個自其他抽象類繼承而來的抽象類時,你應當使用override和abstract指示符將所有的抽象方法重新宣告。Delphi並沒有要求這麼做,因這只是個慣例。這些宣告將清楚地告訴程式碼維護人員有哪些方法是抽象的。否則,維護人員可能對那些方法需要實現而那些方法需要保持抽象感到疑惑。例如:

type


  TBaseAbstract = class


  procedure Method; virtual; abstract;


  end;


  TDerivedAbstract = class(TBaseAbsract)


  procedure Method; override; abstract;


  end;


  TConcrete = class(TDerivedAbstract)


  procedure Method; override;


  end;


類方法或構造器也可以是虛擬的。在Delphi中,類引用是一個真的實體,你可以將它賦值給一個變數,當作引數傳遞,或用作引用來呼叫類方法。如果構造器是虛擬的,則類引用有一個靜態的基類型別,但你可以將一個派生型別的類引用賦值給它。Delphi將在該類的VMT中查詢虛擬構造器,而後呼叫派生類的構造器。,

方法(以及其他函式和過程)可以被過載,也就是說,多個例程可以有相同的名字,但是引數定義必須各不相同。宣告過載方法使用overload指示符。在派生類中可以過載繼承於基類的方法。這種情況下,只有派生的類才需要使用overload指示符。畢竟,基類的作者不可能預見其他的程式設計師何時需要過載一個繼承的方法。如果派生類中沒有使用overload指示符,則基類中的相同名稱的方法被遮蔽。如例2-6所示。

例子2-6:方法的過載

type


  TAuditKind = (auInternal, auExternal, auIRS, auNasty);


  TAccount = class


  public


  procedure Audit;


  end;


  TCheckingAccount = class(TAccount)


  public


  procedure Audit(Kind: TAuditKind); // Hides TAccount.Audit


  end;


  TSavingsAccount = class(TAccount)


  public


  // Can call TSavingsAccount.Audit and TAccount.Audit


  procedure Audit(Kind: TAuditKind); overload;


  end;


var


  Checking: TCheckingAccount;


  Savings: TSavingsAccount;


begin


  Checking := TCheckingAccount.Create;


  Savings := TSavingsAccount.Create;


  Checking.Audit;  // 錯誤,因為TAccount.Audit被遮蔽了。


  Savings.Audit;  //正確,因為Audiot被過載了。


  Savings.Audit(auNasty);  //正確


  Checking.Audit(auInternal);//正確


 


/develop/read_article.?id=10403">PartI


PartIII


 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-976385/,如需轉載,請註明出處,否則將追究法律責任。

相關文章