程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 理解 Delphi 的類(十一) - 深入類中的方法

理解 Delphi 的類(十一) - 深入類中的方法

編輯:Delphi

[1] - 虛方法與動態方法

  方法來到類中, 以前的特點基本都在;

  因為類一般是存在於一個繼承鏈中, 所以就有了一些新的概念, 譬如: 繼承、覆蓋;也有了很多新名稱, 譬如: 靜態方法、虛方法、動態方法、抽象方法、類方法、消息方法.先從虛方法與動態方法開始吧

  //下面的類中就定義了兩個虛方法(virtual)、兩個動態方法(dynamic)TMyClass = class
 procedure Proc1(x,y: Real); virtual;
 function Fun1(x,y: Real): Real; virtual;
 procedure Proc2(x,y: Real); dynamic;
 function Fun2(x,y: Real): Real; dynamic;
end;
//定義成虛方法或動態方法, 就意味著在後來的子類中將要被覆蓋(override), 也就是重寫TBass = class
 procedure Proc(x,y: Real); virtual;
 function Fun(x,y: Real): Real; dynamic;
end;
TChild = class(TBass)
 procedure Proc(x,y: Real); override;
 function Fun(x,y: Real): Real; override;
end;
{正是因為這種機制而形成了多態}

  //那虛方法和動態方法有什麼區別呢?

  每個類都內含著兩個表: 虛方法表(VMT)和動態方法表(DMT);

  VMT 表包含著本類與其所有父類的虛方法 - 那一般會是一個比較龐大的表;

  DMT 表只包含本類的動態方法 - 如果要調用其上層類的動態方法, 只能逐級查找;

  因此, 使用虛方法速度上會有優勢, 使用動態方法會節約內存;

  在 Delphi 初期只有 virtual 而沒有 dynamic ; 後來隨著 VCL 日漸龐大, 才有了 dynamic ;

  譬如類的事件方法一般都是在早期定義, 為了節約空間, 事件方法在 VCL 中基本都定義成了 dynamic ;

  這樣看來: virtual 和 dynamic 並沒有太多區別, 一個側重速度、一個節約空間; 它們是可以互相代替的!

  另外: 因為它們區別不大, 並且是先有 virtual , 所以人們也習慣於把"虛方法"和"動態方法"都稱作"虛方法".

  [2] - 關於覆蓋與重定義

  //標准的覆蓋是這樣的TBass = class
 procedure Proc; virtual; {或用 dynamic}
end;
TChild = class(TBass)
 procedure Proc; override;
end;
//以下幾種情況屬於重定義, 其中例 3-5 還會有編譯提示

  {例1}TBass = class
 procedure Proc;
end;
TChild = class(TBass)
 procedure Proc;
end;
{例2}TBass = class
 procedure Proc;
end;
TChild = class(TBass)
 procedure Proc; virtual;
end;
{例3}TBass = class
 procedure Proc; virtual;
end;
TChild = class(TBass)
 procedure Proc; virtual;
end;
{例4}TBass = class
 procedure Proc; virtual;
end;
TChild = class(TBass)
 procedure Proc;
end;
{例5}TBass = class
 procedure Proc; virtual;
end;
TChild = class(TBass)
 procedure Proc; dynamic;
end;
{上面這五種情況我們盡量不要使用}

  //方法重定義時, 避免編譯提示的辦法

  {辦法1: 使用 reintroduce 指示字}TBass = class
 procedure Proc; virtual;
end;
TChild = class(TBass)
 procedure Proc; reintroduce;
end;
{辦法2: 使用編譯器指令}TBass = class
 procedure Proc; virtual;
end;
{$WARNINGS OFF}
TChild = class(TBass)
 procedure Proc; virtual;
end;
{$WARNINGS ON}
[3] - 方法在實現時的參數省略

unit Unit1;
interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls;
type
 TForm1 = class(TForm)
 end;
TBass = class
 function Fun1(x,y: Integer): Integer; virtual; {Fun1 有 virtual 指示字}
 function Fun2(x,y,z: Integer): Integer;    {Fun2 有三個參數、一個返回值}
end;
var
 Form1: TForm1;
implementation
{$R *.dfm}
{ TBass }
function TBass.Fun1(x, y: Integer): Integer; {方法實現時, 不能帶 virtual 等指示字}
begin
 Result := x + y;
end;
function TBass.Fun2; {方法實現時可以只有函數名; 如果帶參數和返回值, 必須和定義時一樣}
begin
 Result := x + y + z;
end;
end.

  [4] - 訪問限制(或者叫可見性)

  //下面說的數據成員不僅僅指方法

  TMyClass = class(TObject)

  function Fun1: string; {公共區域的數據成員在默認狀態下會歸於 published 區}

  //所謂默認狀態就是編譯指令為{$M+}, 如果是{$M-}這些數據成員會歸於 public 區private
 function Fun2: string; {private 區的數據成員只能在類內訪問}
protected
 function Fun3: string; {protected 區的數據成員只給自己或子孫類訪問}
public
 function Fun4: string; {public 區的數據成員是公開的, 能夠給子孫類或實例化後對象公開使用}
published
 function Fun5: string; {published 區的數據成員首先類似 public; 它同時屬於 RTTI 信息}
 //譬如我們在 Object Inspector 窗口能夠見到的屬性、方法、事件都是屬於 published 區的

  end;

  //Delphi 還有一個叫"友元類"的概念, 就是指在一個單元內的類直接是沒有訪問限制的, 哪怕是在 private 區.{現在的解決方案是使用 strict 保留字}
TMyClass = class(TObject)
 function Fun1: string;
strict private
 function Fun2: string; {strict private 區的數據成員只能是類內部訪問}
strict protected
 function Fun3: string; {strict protected 區的數據成員只能是類或子孫類訪問}
public
 function Fun4: string;
published
 function Fun5: string;
end;
[5] - 靜態方法、類方法、靜態類方法

  //靜態方法是默認的, 如果不是虛方法或純虛方法, 那它就是一個靜態方法.

  //類方法就是通過類名就可以訪問的方法unit Unit1;
interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls;
type
 TForm1 = class(TForm)
  Button1: TButton;
  Button2: TButton;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
 end;
{類方法示例:}
TMyClass = class(TObject)
 class procedure alert(s: string); {類方法只是比靜態方法多了一個 class 指示字}
end;
{
 類方法不能在 private 和 protected 區;
 類方法不能是虛方法;
 類方法只能使用類中的、在對象實例化以前的數據.
}
var
 Form1: TForm1;
implementation
{$R *.dfm}
{ TMyClass }
class procedure TMyClass.alert(s: string);
begin
 ShowMessage(s);
end;
{類方法可以直接使用}
procedure TForm1.Button1Click(Sender: TObject);
begin
 TMyClass.alert('萬一'); {萬一}
end;
{類的對象當然也能使用}
procedure TForm1.Button2Click(Sender: TObject);
var
 MyClass: TMyClass;
begin
 MyClass := TMyClass.Create;
 MyClass.alert('萬一'); {萬一}
 MyClass.Free;
end;
end.
//靜態類方法{現在的 Delphi 不僅僅有類方法, 同時有:
 類變量: class var
 類常量: class const
 類類型: class type
 類屬性: class property
 靜態類方法就是給類屬性來調用的, 它可以存在與私有區(private),
 譬如下面的 SetName 就是一個靜態類方法:
}
TMyClass = class(TObject)
 private
  class var FName: string;
  class procedure SetName(const Value: string); static; {靜態類方法又多了一個 static 指示字}
 published
 class property Name: string read FName write SetName;
end;
[6] - 類中的方法重載

  //類中的方法重載首先具備前面說過的重載相關的所有特點, 如:TMyClass = class(TObject)
 function Fun(s: string): string; overload;
 function Fun(i: Integer): Integer; overload;
 function Fun(x,y: Integer): string; overload;
end;
//但如果是重載父類中的方法, 如果父類中被重載的方法在本類中沒有重載的話, 可以省略 overload 指示字TBass = class(TObject)
 function Fun(s: string): string; {這裡省略了 overload, 當然也可以不省略}
end;
TChild = class(TBass)
 function Fun(i: Integer): Integer; overload;
end;
{這個很好理解, 父類並不知道哪個子類要重載它的哪個方法}

  //published 區中的方法命名要有唯一性, 該區域中的方法不能重載!

  {這樣是不可以的}TMyClass = class(TObject)
published
 function Fun(s: string): string; overload;
 function Fun(i: Integer): Integer; overload;
end;
//這就有一個問題: 公共區域, 在默認情況下是歸屬於 published 區的, 為什麼公共區域的函數可以重載?{
 編譯器會把公共區域中的重載函數自動歸類到 public 區, 也可能會給 published 區一個;
 這是我猜的, 沒有資料可以參考.
 關於這個問題, 我又搞了一個實驗:
}
//在自動生成的 TForm1 類的公共區域添加重載方法:type
 TForm1 = class(TForm)
 procedure alert(s: string); overload;   {No.1}
 procedure alert(s1,s2: string); overload; {No.2}
 private
  { Private declarations }
 public
  { Public declarations }
 end;
//執行編譯後, 只會留下第一個:type
 TForm1 = class(TForm)
 procedure alert(s: string); overload;    {No.1}
 private
  { Private declarations }
 public
  { Public declarations }
 end;
[7] - 關於 inherited

  // inherited 就是調用父類方法的一個特殊命令; 舉例:unit Unit1;
interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls;
type
 TForm1 = class(TForm)
  Button1: TButton;
  procedure Button1Click(Sender: TObject);
 end;
 {建立父類, 類中包括一個函數 Fun 和一個等待覆蓋的虛方法 Proc}
 TBass = class
  procedure Proc; virtual;
  function Fun(x,y: Integer): string;
 end;
 {建立四個子類, 分別覆蓋父類的虛方法}
 TChild1 = class(TBass)
  procedure Proc; override;
 end;
 TChild2 = class(TBass)
  procedure Proc; override;
 end;
 TChild3 = class(TBass)
  procedure Proc; override;
 end;
 TChild4 = class(TBass)
  procedure Proc; override;
 end;
var
 Form1: TForm1;
implementation
{$R *.dfm}
{ TBass }
function TBass.Fun(x, y: Integer): string;
begin
 Result := IntToStr(x + y); {父類函數是返回兩個數的和}
end;
procedure TBass.Proc;
begin
 ShowMessage('TBass');   {父類的虛方法會彈出信息: TBass}
end;
{ TChild1 }
procedure TChild1.Proc;
begin
 inherited Proc;      {調用父類的 Proc 方法}
end;
{ TChild2 }
procedure TChild2.Proc;
begin
 inherited;         {調用父類的同名方法可以省略方法名}
 ShowMessage('TChild2');  {然後彈出自己的信息框}
end;
{ TChild3 }
procedure TChild3.Proc;
begin
 ShowMessage('TChild3');  {先彈出自己的信息框}
 inherited;         {再調用父類的同名方法}
end;
{ TChild4 }
procedure TChild4.Proc;
begin
 ShowMessage(inherited Fun(11,22)); {調用父類的那個求和的函數}
end;
{測試}procedure TForm1.Button1Click(Sender: TObject);
var
 c1: TChild1;
 c2: TChild2;
 c3: TChild3;
 c4: TChild4;
begin
 c1 := TChild1.Create;
 c2 := TChild2.Create;
 c3 := TChild3.Create;
 c4 := TChild4.Create;
 c1.Proc; {顯 示: TBass}
 c2.Proc; {先顯示: TBass;  再顯示: TChild2}
 c3.Proc; {先顯示: TChild3; 再顯示: TBass}
 c4.Proc; {顯 示: 33; 11+22=33嗎}
 c1.Free;
 c2.Free;
 c3.Free;
 c4.Free;
end;
end.
[8] - 抽象方法與抽象類

  //抽象方法類似與接口; 在沒有接口的年代 Delphi 是用抽象方法來模擬接口的; 我想它最終會被接口替代.{下面就定義了兩個抽象方法}
TMyClass = class(TObject)
 procedure Proc1; virtual; abstract;   {抽象方法首先應該是一個虛方法或動態方法}
 function Fun: string; dynamic; abstract; {抽象方法也叫純虛方法}
end;
{
 抽象方法在本類中只有定義、沒有實現;
 抽象方法應該在子類中實現.
 如果一個類包含了抽象方法, 那麼這個類也就成了抽象類;
 抽象類只能通過其子類來實例化, 自己不能生成對象.
 最常用的一個抽象類應該是 TStrings 了, 舉例:
}
var
 List: TStrings;
 i: Integer;
begin
 List := TStringList.Create;
 for i := 0 to 99 do List.Add(IntToStr(i));
 Memo1.Lines := List;
 List.Free;
end;
{
 TStrings 類中包含了抽象方法, 但這些抽象方法在其子類 TStringList 中都得到了實現;
 因此, 我們雖然定義的是 TStrings 類的變量, 卻要通過 TStringList 來實現.
 這裡就有個問題, 我們直接使用 TStringList 不行嗎? 為什麼還要繞個彎?
 我覺得是: 這樣才更符合"多態"的思想吧;
 僅就本例而言 Memo1.Lines 本身就是 TStrings 類型的, 這樣可以避免類型沖突. 舉例說明:
}
//這是個錯誤的例子var
 List: TStringList; {如果定義為 List: TStrings 可消除錯誤}
begin
 List := TStringList.Create;
 List := Memo1.Lines; {這裡會出問題}
 List.Free;
end;
//在 Delphi 7 及以前的版本中, 我們是通過查看一個類是不是有抽象方法來判斷是不是抽象類的.{現在可以用 class abstract 聲明抽象類, 譬如:}
TBass = class abstract(TObject)
 procedure Proc;
 function Fun: string; virtual; abstract;
end;
{但這好像僅僅是個提示, 如果其中沒有抽象方法, class abstract 的定義只是個擺設, 譬如:}
TBass = class abstract(TObject)
 procedure Proc;
 function Fun: string;
end;
{和}
TBass = class(TObject)
 procedure Proc;
 function Fun: string;
end;
{使用起來沒看出區別!}

  [9] - 不能被覆蓋的方法與不能被繼承的類

{
  抽象類是一定要被繼承才可以使用的; 抽象方法是一定要被覆蓋才可以使用的.
  現在說的是不能被覆蓋的方法, 與不能被繼承的類.
}
//譬如:
TA = class
 procedure Proc; virtual; {TA 中的虛方法, 將要被覆蓋}
end;
TB = class(TA)
 procedure Proc; override; {覆蓋}
end;
TC = class(TB)
 procedure Proc; override; {再次覆蓋}
end;
//假如要設定 TB.Proc 為最終方法, 不允許再覆蓋了, 需要 final 指示字.
TA = class
 procedure Proc; virtual; {TA 中的虛方法, 將要被覆蓋}
end;
TB = class(TA)
 procedure Proc; override; final; {最終覆蓋}
end;
TC = class(TB)
 //procedure Proc; override; {再覆蓋不行了}
end;
//用 class sealed 是不能被繼承的
TMyClass = class sealed(TObject)
//...
end;
[10] - 構造方法與析構方法

  //構造方法就是對象建立時調用的方法; 析構方法就是對象銷毀時調用的方法. 如:TMyClass = class(TObject)
public
 constructor Create;      {構造方法}
 destructor Destroy; override; {析構方法}
end;
{幾個要點:
 這兩個方法都可以追溯到所有類的祖先類 TObject;
 它們都屬於類方法, 盡管沒有 class 標識符;
 但和類方法也有區別: 一般的類方法不能使用類中非靜態數據; 但它們可以.
 必須使用 constructor 和 destructor 來定義, 但名稱未必是: Create 與 Destroy;
 如果不使用 Create 與 Destroy 的命名會帶來很多麻煩, 沒必要嘗試.
 我們自己的代碼在 Create 中是執行在父類的 Create 代碼之後; 在 Destroy 中執行在之前;
 所以, 一般會用這樣的語句格式: 
}
constructor TMyClass.Create;
begin
 inherited;
 //...
end;
destructor TMyClass.Destroy;
begin
 //...
 inherited;
end;
{
 Create 是我們用得最多的方法; 但 Destroy 是我們用得最少的方法;
 因為通過 Free 調用 Destroy 更安全一些.
 Destroy 是一個虛方法; Create 在祖先類中雖然是靜態方法, 但在很多實用的類中也成了虛方法;
 所以在覆蓋或重載時應區別對待.
}
// Create 應該是受到編譯器特別對待的一個方法, 譬如:
TMyClass = class(TObject)
public
 constructor Create;
end;
{從道理上來講, 這會隱藏或替換了父類的 Create ; 但實際上沒有, 編譯器肯定要做一個幕後工作!}

  //再如:TMyClass = class(TObject)
public
 constructor Create(x,y: Integer);
end;
var
 Form1: TForm1;
implementation
{$R *.dfm}
{ TMyClass }
constructor TMyClass.Create(x, y: Integer);
begin
 //inherited Create;
 //...
end;
{
 編譯器竟也允許沒有 overload 的重載; 竟也允許去掉這句: inherited Create;
 如果沒有 TObject.Create 方法, 類如何初始化、分配內存?
 所以這都是表面現象, 我想編譯器會自動 overload、自動 inherited Create 的.
 其他方法不會有這些特殊待遇的, 看來哪都有走後門的.
}

  [11] - 事件方法

  在方法的類別中, 應該還有一種事件方法;

  事件是一種特殊的屬性, 使用事件, 就是使用屬性; 然後屬性再調用事件方法.

  到屬性裡面再深入學習吧.

  [12] - 消息方法

  //一個前導示例:{創建一 Win32 工程, 給窗體添加 OnKeyDown 事件}
procedure Tbu.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
 Self.Text := Char(Key);
end;
{功能: 在鍵盤上按一個鍵, 窗體的標題欄會顯示鍵名}

  //現在我們用消息方法重新實現這個功能unit Unit1;
interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs;
type
 TForm1 = class(TForm)
  procedure KeyDown(var msg: TWMKeyDown); message WM_KEYDOWN;
 end;
var
 Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.KeyDown(var msg: TWMKeyDown);
begin
 Self.Text := Char(msg.CharCode);
end;
//解釋一下這個消息方法的定義:procedure KeyDown(var msg: TWMKeyDown); message WM_KEYDOWN;
{
 1、和其他方法的最大不同: 多了一個 message 指示字;
 2、指示字後面是要攔截的消息名稱: WM_KEYDOWN;
 3、它是一個過程, 過程名 KeyDown 是自定義的;
 4、參數類型是消息對應的參數結構, 因為 TWMKeyDown 是 TWMKey 的重命名, 也可以用 TWMKey;
 5、參數名 msg 是自定義的;
 6、參數的前綴必須是 var;
 7、方法實現時不能攜帶指示字.
}
//如果把以上兩個功能放在一起, 當我們按下一個鍵? 會執行哪一個呢?{測試一下}
unit Unit1;
interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs;
type
 TForm1 = class(TForm)
  {窗體 OnKeyDown 事件的定義}
  procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  {WM_KEYDOWN 消息方法的定義}
  procedure KeyDown(var msg: TWMKeyDown); message WM_KEYDOWN;
 end;
var
 Form1: TForm1;
implementation
{$R *.dfm}
{窗體 OnKeyDown 事件的實現}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
 Shift: TShiftState);
begin
 ShowMessage('事件: ' + Char(Key));
end;
{WM_KEYDOWN 消息方法的實現}
procedure TForm1.KeyDown(var msg: TWMKeyDown);
begin
 ShowMessage('消息: ' + Char(msg.CharCode));
end;
end.
{測試結果: 只執行了消息方法, 沒有執行事件, 也就是事件被消息攔截了}

  //可以並存嗎? 當然可以!{把消息的實現改為:}
procedure TForm1.KeyDown(var msg: TWMKeyDown);
begin
 ShowMessage('消息: ' + Char(msg.CharCode));
 inherited;
end;
{會先執行消息, 後調用方法}
{把消息的實現改為:}
procedure TForm1.KeyDown(var msg: TWMKeyDown);
begin
 inherited;
 ShowMessage('消息: ' + Char(msg.CharCode));
end;
{會先調用方法, 後執行消息}
{
 消息這個概念還是非常復雜的,
 譬如, 現在只是攔截了 WM_KEYDOWN , Windows 的消息多著呢;
 譬如, 現在只是攔截了當前窗體的消息, 能接受消息的的對象也多著呢;
 還有 Delphi 定義的類似 TWMKey 這樣的眾多消息結構, 譬如鼠標的消息等等...
 這是一個需要另辟專欄的話題.
 總之消息很強大, 能替代所有事件; 我們研究它就是為了解決事件所不能的事情.
}
[13] - 方法的調用約定

  因為使用方法參數的不同, 所以有了調用約定.

  譬如 Delphi 默認的是從左到右讀取參數; Window API 是從右到左讀取參數.

  如果沒有調用約定, Delphi 就無法使用由 C 語言編寫的 Window API;

  如果沒有調用約定, 別的語言也無法使用由 Delphi 編寫的 DLL 文件中的方法.

指示字 參數讀取順序 參數刪除 參數傳遞 備注 register 左→右 方法返回時自動刪除參數 前三個參數使用CPU的三個寄存器傳遞; 其他使用棧傳遞 速度最快, 是 Delphi 的默認方式 pascal 左→右 方法返回時自動刪除參數 使用棧傳遞參數 為兼容存在 cdecl 右→左 由調用者在調用返回時從棧中刪除參數 使用棧傳遞參數 調用來自用 C 或 C++ 編寫的共享庫, 一般用於非 Windows 操作系統 stdcall 右→左 方法返回時自動刪除參數 使用棧傳遞參數 用於調用 Windows API safecall 右→左 方法返回時自動刪除參數 使用棧傳遞參數 用於調用 Windows 中的雙重接口中的方法(除了繼承自 IInterface 和 IDispatch 的方法) near Win16 位下的產物, 現在不用了 far export


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved