delphi多态

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

剖析Delphi中的多态
问题
多态是面向对象的灵魂所在,理解多态是掌握面向对象技术的关键之一。

但是到底什么是多态?多态有何意义?怎么实现多态?多态的概念我能懂,但不知道如何使用以及什么时候该使用呢?请看本文细细道来。

天地生物(物,即对象),千变万化;而在计算机世界里,只有一行行机器指令,两者似乎毫不相干,过去要用计算机语言来很好地描述现实世界是一件很困难的事情,虽然有人用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;
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 对象名、对象与类的关系
的动态方法的地址,这样就节省了一些内存,但速度不如虚方法,是一种牺牲时间换空间的策略,一般情况不推荐使用。

引用上面的例子来解释一下:
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),而不要管
***********************************************************************}
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;
{如果创建的是窗口且还没有主窗体的话,就把刚创建的窗体设为主窗体}
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=V alue 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}
{References the strings in the list by their positions}
property V alues[const Name: string]: string read GetV alue write SetV alue;
{Represents the value part of a string associated with a given Name, on strings with the form Name=V alue.}…
end;
从Tstrings的定义可以看出,它的大部分Protected和Public的方法都是虚方法或是抽象方法。

(请Soul来补充一些,TstringList->TstringGridString)
2.3其他(请soul来补充)
如果你对多态还不明白的话,那请你记住多态的实质:
“相同的表达式,不同的操作”(就这么简单)
从OOP语言的实现来讲,多态就是使用基类的指针/引用来操作(派生类)对象,在运行期根据实际的对象,来执行不同的操作方法;或者换一种更形象的说法:由对象自己来决定自己操作方式,编译器只需下达做什么的命令(做什么what),而不要管怎么做(how),"怎么做"由为对象自己负责。

这样就实现了接口和实现的分离,使接口重用变得可能。

其实多态也简单!那么使用多态应该注意什么呢?下面我的两点几点建议:
分析业务逻辑,然后把相关的事物抽象为“对象”,再用对象方法封装业务逻辑。

把一些具有多态性的操作,在基类中声明为虚方法(virtual Method),对于在基类没有必要实现的就声明为抽象方法(virtual Abstract Method),然后在其派生类中再覆载它(Override),在使用的时候用基类的引用/指针来调用,这样顺理成章地实现了现实世界中的多态性。

记住千万不要为了多态,而去实现多态,这是一种走形式化的做法,是没有意义的。

由于基类与派生类有一种天然“耦合”关系,修改基类就会导致“牵一发而动全身”,这将是非常麻烦的事情!因此要尽量弱化基类的功能实现,必要时把它设计为“抽象类”,并保证稳定的接口,这可以通过预留一些冗余的虚函数(或抽象函数)来实现。

相关问题
讨论Delphi的多态: /delphibbs/dispq.asp?lid=1753965
关于多态性: /delphibbs/dispq.asp?lid=1854895
什么是多态?在日常编程中有哪些运用?/delphibbs/dispq.asp?lid=960465 overload 与override有何区别,请执教?/delphibbs/dispq.asp?lid=296739
派生类的指针指向基类对象的问题/delphibbs/dispq.asp?lid=2104106
重载的多态
重载允许用相同的运算符或方法,去表示截然不同的意义。

…+‟在上面的程序中有两个意思:两个double型的数相加;两个串相连。

另外还有整型相加,长整型,等等。

这些运算符的重载,依赖于编译器根据上下文做出的选择。

以往的编译器会把操作数隐式转换为完全符合操作符的类型。

虽然Java明确支持重载,但不支持用户定义的操作符重载。

Java支持用户定义的函数重载。

一个类中可以有相同名字的方法,这些方法可以有不同的意义。

这些重载的方法中,必须满足参数数目不同,相同位置上的参数类型不同。

这些不同可以帮助编译器区分不同版本的方法。

编译器以这种唯一表示的特征来表示不同的方法,比用名字表示更为有效。

据此,所有的多态行为都能编译通过。

强制和重载的多态都被分类为特定的多态,因为这些多态都是在特定的意义上的。

这些被划入多态的特性给程序员带来了很大的方便。

强制多态排除了麻烦的类型和编译错误。

重载多态像一块糖,允许程序员用相同的名字表示不同的方法,很方便。

参数的多态
参数多态允许把许多类型抽象成单一的表示。

例如,List抽象类中,描述了一组具有同样特征的对象,提供了一个通用的模板。

你可以通过指定一种类型以重用这个抽象类。

这些参数可以是任何用户定义的类型,大量的用户可以使用这个抽象类,因此参数多态毫无疑问的成为最强大的多态。

乍一看,上面抽象类好像是java.util.List的功能。

然而,Java实际上并不支持真正的安全类型风格的参数多态,这也是java.util.List和java.util的其他集合类是用原始的ng.Object写的原因(参考我的文章"A Primordial Interface?" 以获得更多细节)。

J ava的单根继承方式解决了部分问题,但没有发挥出参数多态的全部功能。

Eric Allen 有一篇精彩的文章“Behold the Power of Parametric Polymorphism”,描述了Java通用类型的需求,并建议给Sun 的Java规格需求#000014号文档"Add Generic Types to the Java Programming Language."(参考资源链接)包含的多态
包含多态通过值的类型和集合的包含关系实现了多态的行为.在包括Java在内的众多面向对象语言中,包含关系是子类型的。

所以,Java的包含多态是子类型的多态。

在早期,Java开发者们所提及的多态就特指子类型的多态。

通过一种面向类型的观点,我们可以看到子类型多态的强大功能。

以下的文章中我们将仔细探讨这个问题。

为简明起见,下文中的多态均指包含多态。

面向类型观点
图1的UML类图给出了类和类型的简单继承关系,以便于解释多态机制。

模型中包含5种类型,4个类和一个接口。

虽然UML中称为类图,我把它看成类型图。

如"Thanks Type and Gentle Class," 一文中所述,每个类和接口都是一种用户定义的类型。

按独立实现的观点(如面向类型的观点),下图中的每个矩形代表一种类型。

从实现方法看,四种类型运用了类的结构,一种运用了接口的结构。

screen.width-600)this.style.width=screen.width-600;">
图1:示范代码的UML类图
以下的代码实现了每个用户定义的数据类型,我把实现写得很简单。

}
/* IType.java */
interface IType
{
String m2( String s ); String m3();
}
/* Derived.java */
public class Derived extends Base implements IType
{
public String m1()
{
return "Derived.m1()"; }
public String m3()
{
return "Derived.m3()"; }
}
/* Derived2.java */
public class Derived2
extends Derived
{
public String m2( String s )
{
return "Derived2.m2( " + s + " )"; }
public String m4()
{
return "Derived2.m4()";
}
}
/* Separate.java */
public class Separate
implements IType
{
public String m1()
{
return "Separate.m1()";
}
public String m2( String s )
{
return "Separate.m2( " + s + " )"; }
public String m3()
{
return "Separate.m3()";
}
用这样的类型声明和类的定义,图2从概念的观点描述了Java指令。

Derived2 derived2 = new Derived2();
screen.width-600)this.style.width=screen.width-600;">
图2 :Derived2 对象上的引用
上文中声明了derived2这个对象,它是Derived2类的。

图2种的最顶层把Derived2引用描述成一个集合的窗口,虽然其下的Derived2对象是可见的。

这里为每个Derived2类型的操作留了一个孔。

Derived2对象的每个操作都去映射适当的代码,按照上面的代码所描述的那样。

例如,Derived2对象映射了在Derived中定义的m1()方法。

而且还重载了Base类的m1()方法。

一个Derived2的引用变量无权访问Base类中被重载的m1()方法。

但这并不意味着不可以用super.m1()的方法调用去使用这个方法。

关系到derived2这个引用的变量,这个代码是不合适的。

Derived2的其他的操作映射同样表明了每种类型操作的代码执行。

既然你有一个Derived2对象,可以用任何一个Derived2类型的变量去引用它。

如图1所示,Derived, Base和IType 都是Derived2的基类。

所以,Base类的引用是很有用的。

图3描述了以下语句的概念观点。

Base base = derived2;
screen.width-600)this.style.width=screen.width-600;">
图3:Base类引用附于Derived2对象之上
虽然Base类的引用不用再访问m3()和m4(),但是却不会改变它Derived2对象的任何特征及操作映射。

无论是变量derived2还是base,其调用m1()或m2(String)所执行的代码都是一样的。

两个引用之所以调用同一个行为,是因为Derived2对象并不知道去调用哪个方法。

对象只知道什么时候调用,它随着继承实现的顺序去执行。

这样的顺序决定了Derived2对象调用Derived里的m1()方法,并调用Derived2里的m2(String)方法。

这种结果取决于对象本身的类型,而不是引用的类型。

尽管如此,但不意味着你用derived2和base引用的效果是完全一样的。

如图3所示,Base的引用只能看到Base 类型拥有的操作。

所以,虽然Derived2有对方法m3()和m4()的映射,但是变量base不能访问这些方法。

运行期的Derived2对象保持了接受m3()和m4()方法的能力。

类型的限制使Base的引用不能在编译期调用这些方法。

编译期的类型检查像一套铠甲,保证了运行期对象只能和正确的操作进行相互作用。

换句话说,类型定义了对象间相互作用的边界。

多态的依附性
类型的一致性是多态的核心。

对象上的每一个引用,静态的类型检查器都要确认这样的依附和其对象的层次是一致的。

当一个引用成功的依附于另一个不同的对象时,有趣的多态现象就产生了。

(严格的说,对象类型是指类的定义。

)你也可以把几个不同的引用依附于同一个对象。

在开始更有趣的场景前,我们先来看一下下面的情况为什么不会产生多态。

多个引用依附于一个对象
图2和图3描述的例子是把两个及两个以上的引用依附于一个对象。

虽然Derived2对象在被依附之后仍保持了变量的类型,但是,图3中的Base类型的引用依附之后,其功能减少了。

结论很明显:把一个基类的引用依附于派生类的对象之上会减少其能力。

一个开发这怎么会选择减少对象能力的方案呢?这种选择是间接的。

假设有一个名为ref的引用依附于一个包含如下方法的类的对象:。

相关文档
最新文档