多态的概念与接口重用

首先,什么是多态(Polymorphisn)?按字面的意思来讲,就是“多种形状”。笔者也
没有找到对多态的非常学术性的描述,暂且引用一下Charlie Calvert对多态的描述——多态
性是允许用户将父对象设置成为与一个或更多的它的子对象相等的技术,赋值之后,基类
对象就可以根据当前赋值给它的派生类对象的特性以不同的方式运作。


更简单地说就是:多态性允许用户将派生类类型的指针赋值给基类类型的指针。多态
性在Object Pascal中是通过虚方法(Virtual Method)实现的。

什么是“虚方法”?虚方法就是允许被其派生类重新定义的方法。派生类重新定义基
类虚方法的做法,称为“覆盖”(override)。

这里有一个初学者经常混淆的概念:覆盖(override)和重载(overload)。如前所述,
覆盖是指派生类重新定义基类的虚方法的方法。而重载,是指允许存在多个同名函数,这
些函数的参数表不同(或许是参数个数不同,或许是参数类型不同,或许两者都不同)。
重载的概念并不属于“面向对象编程”。重载的可能的实现是:编译器根据函数不同的参
数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器
来说)。例如,有两个重载的同名函数

function func(p : integer) : integer; overload;

function func(p : string) : integer; overload;

那么编译器做过修饰后的函数名可能是:int_func、str_func。如果调用

func(2);

func(′hello′);

那么编译器会把这两行代码分别转换成:

int_func(2);

str_func(′hello′);

这两个函数的调用入口地址在编译期间就已经静态(记住:是静态!)确定了。这样
的确定函数调用入口地址的方法称为早绑定。

而覆盖则是:当派生类重定义了基类的虚方法后,由于重定义的派生类的方法地址无
法给出,其调用地址在编译期间便无法确定,故基类指针必须根据赋给它的不同的派生类
指针,在运行期动态地(记住:是动态!)调用属于派生类的虚方法。这样的确定函数调
用地址的方法称为晚绑定。引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,
它就不是多态”。

..注意:重载只是一种语言特性,与多态无关,与面向对象也无关!

多态是通过虚方法实现的,而虚方法是通过晚绑定(或动态绑定)实现的。

其次,多态的作用是什么呢?前两节已经讲到,封装可以隐藏实现细节,使得代码模
块化;继承可以扩展已存在的代码模块,它们的目的都是为了代码重用。而多态则是为了
实现另一个目的——接口重用。

什么是接口重用?举一个简单的例子,假设有一个描述飞机的基类:


 

type

 TPlane = class

 protected

 FModal : String; // 型号

 public

 procedure fly(); virtual; abstract; // 起飞抽象方法

 procedure land(); virtual; abstract; // 着陆抽象方法

 function modal() : string; virtual; // 查寻型号虚方法

 …… // 其他可能的操作

 end;

然后,从TPlane派生出两个派生类,直升机(TCopter)和喷气式飞机(TJet):

 TCopter = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 function modol() : string; override;

 …… //其他可能的操作

end;

 TJet = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 …… //其他可能的操作,没有覆盖modal()

 end;

TPlane类的声明中,fly和land方法都是被声明为virtual和abstract的,这是向编译器
指出这些方法是抽象(纯虚)的,也就是在TPlane类中不提供这些方法的实现,而派生类
则必须实现它,即规定了一套接口。凡是含有abstract方法的类被称为“抽象类”,永远无
法创建抽象类的实例对象。抽象类是被用来作为接口的。

现在,假设要完成一个机场管理系统,在有了以上的TPlane之后,再编写一个全局的
函数g_FlyPlane(),就可以让所有传递给它的飞机起飞:


procedure g_FlyPlane(const Plane : TPlane);

begin

 Plane.fly();

end;

是的,仅仅如此就可以让所有传给它的飞机(TPlane的派生类对象)正常起飞!不管
是直升机还是喷气式飞机,甚至是现在还不存在的、以后会增加的飞碟。这是因为,每个
派生类(真正的飞机)都可以通过“override”来定义适合自己的起飞方式。

可以看到,g_FlyPlane()函数接受的参数是TPlane类对象的引用,而实际传递给它的都
是 TPlane的派生类对象。现在回想一下本节开头所描述的“多态”:多态性是允许将父对
象设置成为与一个或更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋
值给它的子对象的特性以不同的方式运作。很显然,

parent := child;

就是多态的实质!这里的飞机类(TPlane)作为一种接口,而该接口就是被重用的目标。

多态的本质就是“将派生类类型的指针赋值给基类类型的指针”(在Object Pascal中
是引用),只要这样的赋值发生了,就是在应用多态了,因为在此实行了“向上映射”(“上
下”是指类继承层次关系)。

应用多态的例子非常普遍。在Delphi的VCL类库中,最典型的就是:TObject类有一
个虚拟的Destroy析构函数和一个非虚拟的Free方法。Free方法中首先判断对象本身是否
为nil,保证不为nil时便调用Destroy。对任何对象(都是TObject的派生类对象)调用其
Free();方法,但执行的都是TObject.Free();(因为TObject.Free()为非虚拟方法,无法被覆盖),
然后由它调用被每个类重定义了的析构函数Destroy();(因为Destroy()为虚方法,派生类可
以覆盖),这就保证了任何类型的对象都可以正确、安全地被析构。

因此,在定义自己的类时,如果有析构函数存在,就必须在它的声明之后加上override
关键字。否则会发生什么呢?

还拿刚才的飞机类作为例子,有一个飞机销毁站,这个销毁站有一个函数:

procedure DestroyPlane(var Plane : TPlane)

begin

 Plane.Free();

 Plane := nil

end;

将正常的飞机(析构函数带有override的飞机)传给它,编译器都会正常地调用飞机
的析构函数用以将飞机拆解开,将资源正常回收。

如果建造的飞机的析构函数没有被指明override关键字,那么,将飞机传递给这个
DestroyPlane()的函数时,编译器调用的绝对不是用户所给飞机定义的析构函数,而会是
TPlane.Destroy()。能指望TPlane的析构函数会好好拆解飞机吗?祈祷吧,别把油箱弄爆


炸了。

也就是说,在被执行了

parent := child;

之后,无论调用

parent.Free();

还是调用

child.Free();

都应该产生同样的结果,从语义上来说,这两行代码必须做相同的事情。当然,这样的情
况不仅仅只对于析构函数而言,任何想要使通过基类对象指针做到的事情与通过派生类对
象指针所做的相同,就要在基类中将这个方法声明为virtual,在派生类中将该方法声明为
override。

..注意:给自己的析构函数加上override声明!

原文地址:https://www.cnblogs.com/zhangzhifeng/p/2101085.html