轉貼
作者:MajorSoft
1什麼是多态?
1.1概念
1.2多态的意義
1.3多态在delphi中如何實作的?
1.3.1 繼承(Inheritance)
1.3.2 虛方法、動态方法與抽象方法,VMT/DMT,靜态綁定與動态綁定
1.3.3 重載(Overload)與多态
1.4多态種類的探讨
1.4.1 兩級多态
1.4.2 不安全的多态
2 VCL中多态的應用
2.1構造與析構方法
2.2 Tstrings
2.3其他(請soul來補充)
摘 要 多态是面向對象的靈魂所在,了解多态是掌握面向對象技術的關鍵之一,本文着重分析多态的基本原理、多态的實質以及在VCL中的應用。
關鍵字 多态、繼承、面向對象、VCL、虛函數(virtual Method)、覆載(override)
問題
多态是面向對象的靈魂所在,了解多态是掌握面向對象技術的關鍵之一。但是到底什麼是多态?多态有何意義?怎麼實作多态?多态的概念我能懂,但不知道如何使用以及什麼時候該使用呢?請看本文細細道來。
專家分析
天地生物(物,即對象),千變萬化;而在計算機世界裡,隻有一行行機器指令,兩者似乎毫不相幹,過去要用計算機語言來很好地描述現實世界是一件很困難的事 情,雖然有人用C語言寫出面向對象的程式來,但我敢斷定其寫法是極其煩瑣的,直到面向對象(Oriented-Object 簡稱OO)的出現,一切都随之改觀,整個軟體業發生了翻天覆地的變化,從程式設計語言的變化開始,出現了一系列面向對象程式設計語言(OOP)如 SmallTalk、C++、Java、Object Pascal、C#等;随之各種面向對象開發工具也出現了如VC、Delphi、BCB、JBuilder等,并出現了許多優秀的類庫如VCL、.net Framework和一些商業類庫等;再發展到了面向對象的設計(OOD),面向對象的分析(OOA)以及面向對象的資料庫(OODB),面向對象技術幾 乎貫穿了整個軟體領域,程式員的思考方式也發生了根本性的變化!在一些OO純化論者眼中,一切皆是對象!雖然我不完全同意這種看法。但我認為這種方式最符 合人們的思維習慣,它使程式員能集中精力考慮業務邏輯,由計算機來完成面向對象到機器指令的轉換(由面向對象的編譯器來完成),程式員的大腦從此解放出來 了!這是一場革命!
面向對象的核心内容是對象,封裝,繼承,多态和消息機制,其中多态就是為了描述現實世界的多樣性的,也是面向對象中最為重要的特性,可以這麼說,不掌握多态,就沒有真正地掌握面向對象技術。
1什麼是多态?
1.1概念
多态的概念衆說紛纭,下面是幾種代表性的說法:
“This ability to manipulate more than one type with a pointer or a reference to a base classis spoken of as polymorphism” (《C++ Primer》第838頁)。即用基類的指針/引用來操作多種類(基類和其派生類)的對象的能力稱之為多态。它是從語言實作的角度來考慮的。
“polymorphism provides another dimension of separation of interface from implementation, to decouple what from how”(《Think in Java》3rd edtion),即多态提供了另外一種分離接口和實作(即把“做什麼”與“怎麼做”分開)的一種尺度。它是從設計的角度考慮的。
“The ability to use the same expression to denote different operations is refered to as Polymorphism”,(《Object-Oriented Methods Principles & Practice》3rd Edition,第16頁)。簡單的說,多态就是“相同的表達式,不同的操作”,也可以說成“相同的指令,不同的操作”。這是從面向對象的語義的角度來看 的。
三種說法分别從不同的角度來闡述了多态的實質。其中第三種說法尤為确切,下面着重分析第三種說法。
先解釋這句話的含義:
相同的表達式—函數調用
不同的操作 —根據不同的對象就有不同的操作。
舉個例子來說明,比如在公司中有各種職責不同的員工(程式員,業務員,文管等),他們“上班”時,做不同的事情(也可以看作是一種業務邏輯),我們把他們各自的工作都抽象為"上班",關系如下:
員工
/ | / ——繼承關系
程式員 業務員 文管
每天上班時間一到,相當于發了一條這樣的指令:
“員工們.開始上班”(同一條表達式)
每個員工接到這條指令(同樣的指令)後,就“開始上班”,但是他們做的是各自的工作,程式員就開始“Coding”,業務員就開始“聯系業務”,文管員就開始“整理文檔”。即“相同的表達式(函數調用),(在運作期根據不同的對象來執行)不同的操作”。
從語言實作多态的角度來說,多态是通過基類指針或引用指向派生類的對象,調用其虛方法實作的。下面是Object Pascal語言的實作
TEmployee=class //把員工抽象為一個抽象類
public
procedure startWorking;virtual;abstract;
{抽象函數(即C++中純虛函數),什麼也不做,實際的意義是,先預留一個接口。在其派生類中覆載實作它。}
end;
TProgramer=class(TEmployee) //程式員
public
procedure startWorking;override;
end;
TBusinessMan=class(TEmployee) //業務員
public
procedure startWorking;override;
end;
TDocManager=class(TEmployee) //文管
public
procedure startWorking;override;
end;
procedure TProgramer.startWorking;
begin
showmessage('coding');
end;
{ TbusinessMan }
procedure TbusinessMan.startWorking;
begin
showmessage('Linking Business');
end;
{ TDocManager }
procedure TDocManager.startWorking;
begin
showmessage('Managing Document');
end;
procedure TForm1.Button1Click(Sender: TObject);
const
eNum=3;
var
Employee:array of TEmployee;
i:integer;
begin
setLength(Employee,eNum);
Employee[0]:=TProgramer.Create;
//把基類引用employee[0]指向剛建立的TProgramer對象
Employee[1]:=TBusinessMan.Create;
//把基類引用employee[1]指向剛建立的TBusinessMan對象
Employee[2]:=TDocManager.Create;
//把基類引用employee[2]指向剛建立的TDocManager對象
for i:=0 to Length(Employee)-1 do
Employee[i].startWorking; //在運作期根據實際的對象類型動态綁定相應的方法。
{從語言實作多态的角度來說,多态是通過基類指針或引用指向派生類的對象,調用其虛方法來實作的。Employee []為基類對象引用數組,其成員分别指向不同的派生類對象,當調用虛方法,就實作了多态}
end;
試一試
大家可以敲入上面一些代碼(或Demo程式),并編譯運作,單擊按扭就可以看多态性的神奇效果了。
1.2多态的意義
封裝和繼承的意義是它們實作了代碼重用,而多态的意義在于,它實作了接口重用(同一的表達式),接口重用帶來的好處是程式更易于擴充,代碼重用更加友善,更具有靈活性,也就能真實地反映現實世界。
比如為了更好地管理,把程式員分為C++程式員,Delphi程式員。…
員工
/ | / ——繼承關系
程式員 業務員 文管
/ / ——繼承關系
C++程式員 Delphi程式員
在程式員添加TCppProgramer,TDelphiProgramer兩個派生類後,調用的方式還是沒有變,還是“員工們.開始上班”,用Object Pascal來描述:
…
setLength(Employee,eNum+2);
Employee[ENum]:=TCppProgramer.create;
//建立一個TcppProgramer對象,并把基類引用employee[ENum]指向它
Employee[eNum+1]:=TDelphiProgramer.Create;
…
{員工們.開始上班}
for i:=0 to Length(Employee)-1 do
Employee[i].startWorking; //還是同一的調用方法(因為接口并沒變)。
…
1.3多态在delphi中如何實作的?
實作多态的必要條件是繼承,虛方法,動态綁定(或滞後聯編),在Delphi是怎麼實作多态的呢?
1.3.1 繼承(Inheritance)
繼 承指類和類之間的“AKO(A Kind Of,是一種)”關系,如程式員“是一種”員工表示一種繼承關系。在Delphi中,隻支援單繼承(不考慮由接口實作的多重繼承),這樣雖然沒有多繼承的 那種靈活性,但給我們帶來了極大的好處,由此我們可以在任意出現基類對象的地方都可以用派生類對象來代替(反之不然),這也就是所謂的“多态置換原則”, 我們就可以把派生類的對象的位址賦給基類的指針/引用,為實作多态提供了先決條件。
提 示
在UML中:
AKO: A Kind Of 表示繼承(Inheritance)關系
APO: A Part Of 表示組合(Composition)關系
IsA: Is A表示對象和所屬類的關系
1.3.2 虛方法、動态方法與抽象方法,VMT/DMT,靜态綁定與動态綁定
對 于所有的方法而言,在對象中是沒有任何蹤影的。其方法指針(入口位址)儲存在類中,實際代碼則存儲在代碼段。對于靜态方法(非虛方法),在編譯時由編譯器 直接根據對象的引用類型确定對象方法的入口位址,這就是所謂的靜态綁定;而對于虛方法由于它可能覆載了,在編譯時編譯器無法确定實際所屬的類,是以隻有在 運作期通過VMT表入口位址(即對象的首四個位元組)确定方法的入口位址,這就是所謂的動态綁定(或滞後聯編)。
虛方法
虛方法,表示一種可以被覆載(Override)的方法,若沒有聲明為抽象方法,就要求在基類中提供一個預設實作。類中除存儲了自己虛方法指針,還存儲所有基類的虛方法指針。
聲明方法:
procedure 方法名;virtual;
這樣,相當于告訴Delphi編譯器:
可以在派生類中進行覆載(Override)該方法,覆載(Override)後還是虛方法。
不要編譯期時确定方法的入口位址。而在運作期,通過動态綁定來确定方法的入口位址。
在基類中提供一個預設實作,如果派生類中沒有覆載(Override)該方法,就使用基類中的預設實作。
動态方法
動态方法和虛方法本質上是一樣的,與虛方法不同的是,動态方法在類中隻存儲自身動态方法指針,是以虛拟方法比動态方法用的記憶體要多,但它執行得比較快。但這對使用者完全是透明的。
聲明方法:
procedure 過程名;dynamic;
抽象方法
一種特殊的虛方法,在基類它不需提供預設實作,隻是一個調用的接口用,相當于C++中的純虛函數。含有抽象方法的類,稱之為抽象類。
聲明方法:
procedure 過程名;virtual;abstract;
VMT/DMT
在Delphi 中,虛拟方法表(Virtual Method Table,VMT),其實在實體上本沒有,是為了更好地闡述多态,人為地在邏輯上給了它一個定義,實際上它隻是類中的虛方法的位址的集合,這個集合中還 包括其基類的的虛方法。在對象的首四個位元組中存儲的“Vmt 入口位址”,實際上就是其所屬的類的位址(參考Demo程式)。有了實際的類,和方法名就可以找到虛方法位址了。
Obj(對象名) 實際的對象 所屬的類
Vmt 入口位址
資料成員
類虛方法表vmt入口位址
資料成員模闆資訊
靜态方法等
虛方法(VMT)
動态方法(DMT)
圖3 對象名、對象與類的關系
DMT和VMT類似,也是邏輯上的一個概念,不同的是,在類中隻儲存了自身動态方法指針,而沒有基類的動态方法的位址,這樣就節省了一些記憶體,但速度不如虛方法,是一種犧牲時間換空間的政策,一般情況不推薦使用。
引用上面的例子來解釋一下:
Employee[i].startWorking;
Employee [i]是一個基類Temployee的對象引用,有上面的程式知道,它可能指向了一個Tprogramer對象,也可以可能指向一個 TbusinessMan,還有可能是其他的對象,而且這些都是不确定的、動态的,是以在編譯時無法知道實際的對象,也就無法确定方法位址。而在運作期, 當然知道對象的“廬山真面目”了,根據實際對象的首四個位元組的内容,也就是虛拟方法表VMT的入口位址,找到實際要調用的函數,即實作了多态。
1.3.3 重載(Overload)與多态
很 多網友認為函數重載也是一種多态,其實不然。對于“不同的操作”,重載無法提供同一的調用的方式,雖然函數名相同,但其參數不同!實作多态的前提,是相同 的表達式!如Employee[i].startWoring,而重載的調用,有不同的參數或參數類型。重載隻是一種語言機制,C語言中也有重載,但C語 言沒有多态性,C語言也不是面向對象程式設計語言。除非重載函數同時還虛方法,不然編譯器就可以根據參數的類型就可以确定函數的入口位址了,還是靜态綁定!引 用C++之父的話“不要犯傻,如果不是動态綁定,就不是多态”。
1.4多态種類的探讨
1.4.1 兩級多态
對象級:用基類指針/引用指向其派生類對象,調用虛方法(或動态方法、抽象方法),這是用的最多一種。
類級:用類引用(指向類而不是對象的引用)指向派生類,調用虛類方法(或動态類方法、抽象類方法),常用在對象建立的多态性(因為構造方法是一種“特殊的”類方法,請參考我的另一篇拙作《剖析Delphi中的構造和析構》,第2.1節)。
提 示
類引用,是類本身的引用變量,而不是類,更不是對象引用。就和對象名表示對象引用一樣,類名就表示一個類引用,因為在Delphi中,類也是作為對象處理的。類引用類型就是類引用的類型,類引用類型的聲明方法:
類引用類型名稱=class of 類名
我們在VCL的源代碼中可以看到很多的類引用的聲明,如:
TClass=class of Tobject;
TComponentClass=class of Tcomponent;
TControlClass=class of Tcontrol;
注 意
在類方法中,方法中隐含的self,是一個類引用,而不是對象引用。
1.4.2 不安全的多态
用派生類指針/引用指向基類對象也可以實作多态!雖然這是一種錯誤的使用方法:
procedure TForm1.btnBadPolyClick(Sender: TObject);
var
cppProgramer:TCppProgramer;//定義一個cpp程式員引用,一個派生類的引用!
begin
{*****************************聲 明***********************************
用派生類指針/引用指向基類對象實作的多态。是一種病态的多态!
這種多态的使用方法,它就象一個實際很小的事物(基類對象)披上一個強大
的外表(派生類引用),因而帶來了許多潛在的不安全因素(如通路異常),所
以幾乎沒有任何價值。"杜撰"這樣一個例子,旨在說明在Delphi中的多态的本質,多态的本質:使用一個合法的(通常是基類的)指針/引用來操作對象, 在運作期根據實際的對象,來執行不同的操作方法,或者更形象的說法:由對象自己來決定自己操作方式,編譯器隻需下達做什麼的指令(做什麼what),而不 要管怎麼做(how),"怎麼做"由為對象自己負責。這樣實作了接口和實作的分離,使接口重用變得可能。
***********************************************************************}
cppProgramer:=TCppProgramer(TProgramer.Create);
{為了實作這種病态的多态,把對象引用強制轉換為TCppProgramer類型,
進而逃過編譯器的檢查}
cppProgramer.startWorking;
{調用的TProgramer.startWorking而不是TcppProgramer.startWorking
這就是用派生類指針/引用指向基類對象實作的多态。}
cppProgramer.Free;
cppProgramer:=TCppProgramer(TDocManager.Create);
cppProgramer.startWorking;
{調用的竟然是TDocManager.startWorking,
這就是用派生類指針/引用指向基類對象實作的多态。這種方法極不安全,
而且沒有什麼必要}
cppProgramer.Free;
end;
試一試
為獲得這種多态的感性認識,建議動手試試,上面說到這種使用方法會有潛在的不安全性(如通路異常),而上面的程式運作一點錯誤都沒有出現,想想為什麼?什麼情況下會出現通路異常,動手寫個通路異常的例子,你将收獲更多。(參考Demo程式)
2 VCL中多态的應用
2.1構造與析構方法
構造方法的多态
由于構造方法可以看作“特殊的”類方法,在Tcomponent之後的所有的派生類的又被重新定義為虛類方法,是以要實作構造方法的多态性,就得使用類引用,在Delphi中有個經典的例子,就在每一個工程檔案中都有一個類似下面的代碼:
Application.CreateForm(TForm1, Form1);
其方法的定義:
procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference);
var// InstanceClass為類引用。
Instance: TComponent;
begin
Instance := TComponent(InstanceClass.NewInstance);
{NewInstance 方法的聲明:class function NewInstance: TObject; virtual; (system單元 432行)是一個類方法,同時也是虛方法,我們把它稱之為虛類方法。InstanceClass是一個類引用,實作了類一級的多态,進而實作了建立元件的 接口重用}
TComponent(Reference) := Instance;
try
Instance.Create(Self);//調用構造方法,進行初始化
except
TComponent(Reference):= nil;//消除“野“指針!good
raise;
end;
{如果建立的是視窗且還沒有主窗體的話,就把剛建立的窗體設為主窗體}
if (FMainForm = nil) and (Instance is TForm) then
begin
TForm(Instance).HandleNeeded;
FMainForm := TForm(Instance);//設定主窗體
{ 實際上,在項目選項(project->options)中設定主窗體,實際上就把工程檔案中相應的窗體語句提到所有建立窗體語句之前。}
end;
end;
2) 析構方法的多态請參考《剖析Delphi中的構造和析構》,第3.3節
2.2 Tstrings
字元串數組處理在Delphi的控件中十分常見,通常是一些Items屬性,我們用起來也特别地友善(因為都是一樣的使用接口),這得益于Delphi中字元串數組的架構的設計。這是一個成功的設計。
由 于很多控件中要用到字元串數組,如ComboBox,TstringGrid等等,但每個控件中的字元串數組又不同,Delphi由此把字元串數組但抽象 出來,進而出現了很多與之相關的類。其中基類Tstrings隻是提供為各種調用提供接口,具體實作完全可由其派生類中實作,是以,把Tstrings定 義為抽象類。
下面就來看看基類TStrings類的常用方法的定義(參見Classes單元第442行):
TStrings = class(TPersistent)
protected
...
function Get(Index: Integer): string; virtual; abstract;
procedure Put(Index: Integer; const S: string); virtual;
function GetCount: Integer; virtual; abstract;
…
public
function Add(const S: string): Integer; virtual; //實際調用的是Insert
{添加一字元串S到字元串清單末尾}
procedure AddStrings(Strings: TStrings); virtual;
{添加字元串清單Strings到該字元串清單末尾}
procedure Insert(Index: Integer; const S: string); virtual; abstract;
{抽象方法,在第Index位置插入一新字元串S}
procedure Clear; virtual; abstract;
{清除所有的字元串}
procedure Delete(Index: Integer); virtual; abstract;
{删除某個位置上的字元串}
function IndexOf(const S: string): Integer; virtual;
{擷取S在字元串清單中的位置}
function IndexOfName(const Name: string): Integer; virtual;
{Returns the position of the first string with the form Name=Value with the specified name part}
function IndexOfObject(AObject: TObject): Integer; virtual;
{擷取對象名為AObject:的對象在字元串清單中的位置}
procedure LoadFromFile(const FileName: string); virtual;
{Fills the list with the lines of text in a specified file}
procedure LoadFromStream(Stream: TStream); virtual;
{Fills the list with lines of text read from a stream}
procedure SaveToStream(Stream: TStream); virtual;
{Writes the value of the Text property to a stream object}
property Strings[Index: Integer]: string read Get write Put; default;
{References the strings in the list by their positions}
property Values[const Name: string]: string read GetValue write SetValue;
{Represents the value part of a string associated with a given Name, on strings with the form Name=Value.}
…
end;
從Tstrings的定義可以看出,它的大部分Protected和Public的方法都是虛方法或是抽象方法。(請Soul來補充一些,TstringList->TstringGridString)
2.3其他(請soul來補充)
如果你對多态還不明白的話,那請你記住多态的實質:
“相同的表達式,不同的操作”(就這麼簡單)
從OOP 語言的實作來講,多态就是使用基類的指針/引用來操作(派生類)對象,在運作期根據實際的對象,來執行不同的操作方法;或者換一種更形象的說法:由對象自 己來決定自己操作方式,編譯器隻需下達做什麼的指令(做什麼what),而不要管怎麼做(how),"怎麼做"由為對象自己負責。這樣就實作了接口和實作 的分離,使接口重用變得可能。
其實多态也簡單!那麼使用多态應該注意什麼呢?下面我的兩點幾點建議:
分析業務邏輯,然後把相關的事物抽象 為“對象”,再用對象方法封裝業務邏輯。把一些具有多态性的操作,在基類中聲明為虛方法(virtual Method),對于在基類沒有必要實作的就聲明為抽象方法(virtual Abstract Method),然後在其派生類中再覆載它(Override),在使用的時候用基類的引用/指針來調用,這樣順理成章地實作了現實世界中的多态性。記住 千萬不要為了多态,而去實作多态,這是一種走形式化的做法,是沒有意義的。
由于基類與派生類有一種天然“耦合”關系,修改基類就會導緻“牽一發而動全身”,這将是非常麻煩的事情!是以要盡量弱化基類的功能實作,必要時把它設計為“抽象類”,并保證穩定的接口,這可以通過預留一些備援的虛函數(或抽象函數)來實作。
相關問題
讨論Delphi的多态: http://www.delphibbs.com/delphibbs/dispq.asp?lid=1753965
關于多态性: http://www.delphibbs.com/delphibbs/dispq.asp?lid=1854895
什麼是多态?在日常程式設計中有哪些運用?http://www.delphibbs.com/delphibbs/dispq.asp?lid=960465
overload 與 override有何差別,請執教?http://www.delphibbs.com/delphibbs/dispq.asp?lid=296739
派生類的指針指向基類對象的問題 http://www.delphibbs.com/delphibbs/dispq.asp?lid=2104106
(最後一個問題是我在深入學習多态時在DelphiBBS上提的,曾引起熱烈的讨論,建議看看)