Delphi面向对象(OOP)

何为面向对象?(OO)

结构化编程(SP)是一种编程方法,它是用计算机的视角来分析问题。面“面向对象编程”(OOP)也是一种编程方法,它从更接近真实世界的视角来分析问题,使用更接近人们理解真实世界的方法来抽象问题,这种方法称为面向对象。

何为面向对象编程?(OOP)

“面向对象”这个词代表的是一种认识世界、分析问题、解决问题的方法,因此它是一种方法论。而面向对象编程则是将之应用于编程的方法。

Delphi 是面向对象编程的语言,使用Delphi进行编程,就必须理解一些基本的东西。

类、对象

概念: “类”是对一类事物的抽象(abstract),是创建对象的模版;“对象”是类的实例(instance)。

语言的“类”和“对象”:

从语言的角度来说,“类”是用户定义的数据类型,“对象”是“类”型的变量。类定义了所生成的对象的模版,于是也决定了对象所占用的内存空间。当用 class 关键字声明一个类型时,就创建了一个类。

type
TMyClass = class;
end;

构造函数:定义构造函数使用Constructor关键字。按惯例,构造函数名称为Create(当然也可以用其他名称,但那绝非优良的设计)。

析构函数:定义析构函数使用Destructor关键字。按惯例,析构函数名称为Destroy。如:
type
TMyClass = class
Public
Destructor Destroy(); override;
End;

之所以在析构函数声明最后加上override声明,是为了保证在多态的情况下对象能正确被析构。如果不加override关键字,则编译器会给出类似“Method ‘Destroy’ hides virtual method of base type ‘TObject’”的警告提示。警告的意思是用户定义的Destroy隐藏了基类的虚方法TObject.Destroy(),那样的话,在多态的情况下就无法正确析构对象了。

注意:析构函数都需要加override声明。在析构对象的时候,应该调用对象的Free()方法而不是直接调用Destroy()。

“类方法”和“类引用”类型:一般的“方法”都是指“对象方法”,也就是说,执行该方法,可以导致对象的状态发生改变,即该方法可以更改对象的数据成员的值。除了“对象方法”外,还有所谓的“类方法”,也就是属于类级别的函数(而非对象级别的)。它可以改变类的状态(而非对象的状态)。

定义一个类方法是一般方法的声有前加上关键字 class ,如:

class function TObject.ClassName: ShortString;

既然类方法是进行类级别的操作,因此在类方法中是无法对对象的数据成员进行访问的。

在Object Pascal中,还有一种“类之类”的类型,也就是所谓的“类引用”。一般所称的类,是对其实例对象的抽象。定义一个类:

TMyClass = class;

而“类引用”类型却是对“类”的抽象(元类),所以被称为“类之类”。定义一个“类之类”:

TMyClassClass = class of TMyClass;

“类之类”可以直接调用“类”的“类方法”。

“类方法”和“类引用”有什么作用呢?它主要用在类型参数化上,因为有时在编译时无法得知某个对象的具体类型,而需要调用其类方法(如构造函数),此时可以将类型作为一个参数来传递。

语义的“类”和“对象”:

从语义的角度来说,“类”是对一种逻辑的抽象,而“对象”则是这种逻辑的具体实例。带一些感情色彩地说,“类”与其他数据类型相比,更像是活的,具有生命特征。一旦类的对象被构造了,这个对象就具有了生命,如同现实中的一个社会成员。它有自己的生存空间,有自己的私有特性,能向用户提供服务,还可以和用户交流……

优秀的设计,有几个需要注意的:

1. 类必须有表示其实例对象的状态的数据成员。
2. 在面向对象的世界中,类的使用者比类的构造者要多得多,如果想让别人用自己的类,就要让类容易被使用。
3. 具有状态信息,接口简单、明了,易用。

封装

何为封装? 封装, 是抽象数据类型(或基于对象)的特性。

为什么要封装?

可以把程序按某种方法分成很多“块”,块与块之间可能会有联系。每个块都有一个可变的部分和一个稳定的部分。我们需要把可变的部分和稳定的部分分离开来,将稳定的部分暴露给其他块,而将可变的部分隐藏起来,以便于随时可以让它改变。这项工作就是封装!

在Object Pascal中,实现了两个级别的封装:类级和单元级。

类级别的封装:

类级别的封装是最常见的封装形式。

每个Object Pascal的类,有四种访问级别:private、protected、public、published。其中,public的成员可以被外界的所有客户代码直接访问;published和public差不多,区别仅在于published的成员可以被Delphi开发环境的Object Inspector所显示,因此一般将属性或事件声明于published段;private成员为类的私有性质,仅有类本身和友元可访问;protected成员基本与private类似,区别在于protected可以被该类的所有派生类访问。

在类级别的封装中,对外界的接口是public方法和published成员的集合,private和protected的集合则属于类的实现细节。而对于该类的派生类来说,接口是public、published与protected的集合,而只有private部分为内部实现细节。

单元级别的封装:

单元级别的封装包含的含义有:
1.在一个Unit中声明的多个类,互为友元类。
2.在一个Unit的interface部分声明的变量为全局变量,其他Unit可见。
3.在一个Unit的implementation部分声明的变量为该unit的局部变量,只在该Unit可见。
4.每个Unit可有单独的初始化段(initialization)和反初始化段(finalization),可在编译器支持下自动进行Unit级别的初始化和反初始化。

Object Pascal规定,声明在同一个Unit之中的多个类互为友元类,友元类之间可以互相访问所有数据,无论是public的,还是private的,或者是protected的。也就是说,友元类之间没有秘密。

Object Pascal的单元文件被分成了两个部分:interface和implementation。如同类的封装一样,Unit的这两部分分别为接口和实现细节。因此,interface部分对外是可见的,声明在interface段中的所有函数、过程、变量的集合,即单元文件作为一个模块的对外接口,而implementation部分对外是隐藏的。

而为单元文件提供初始化和反初始化机制,则保证了单元的独立性,其作用如同类的构造函数与析构函数,单元的运作由此便可脱离对其他模块的依赖。

无论是单元的封装,还是类的封装,封装的目的都是一样的,即简化用户接口,隐藏实现细节。封装的难点在于如何设计接口。封装的原则:一旦接口被公布,永远也不要改变它!

继承

继承是为了表现类和类之间的“是一种”关系。有了继承之后,构建多层次的类框架成为可能。同时,它也是面向对象中的另一个核心概念——多态的存在基础。继承是面向对象语言必不可少的特性,只支持封装而不支持继承的语言只能称为“基于对象”(Object-Based)而非“面向对象”(Object-Oriented)。

语言的“继承”:

继承关系也被称为派生。继承的关系中,被继承的称为基类;从基类继承而得的,称为派生类。比如说,类B从类A继承而得,则B为派生类,A为基类。在Object Pascal语言中,定义继承关系的语法:

TB = class(TA)

表示TB从TA继承(派生),TB是派生类,而TA为基类。

Object Pascal只支持单继承,即每个派生类只能有一个基类,由此可以保证每个派生类中,只有惟一一份基类子对象。注意:每个基类子对象都是完整的。

语义的“继承”:

了解语言对于继承的理解与实现支持,对于设计是有所助益的。但是,设计更多的时侯是根据语义的。

上的“继承”,更多的是作为一种“特化”的机制。也就是说,能够被继承的类(基类)总是含有并且只含有所抽象的那一类事物的共性,当需要抽象该类事物中的某一种特例时,将表示特例的类从基类继承(派生),派生类含有这类事物的所有共性,并且自己拥有特性。

语义上的“继承”表示“是一种”的关系,派生类可以被看作“是一种”基类,这是一个最基本的、必须满足的前提。

注意:使得基类弱一些,派生类强一些。

多态

一个抽象的指令,可以让每个个体分别完成具有同一性质但不同内容的动作,多神奇啊!

这就是多态——面向对象编程的核心概念。为了能让读者先对多态抱有足够的重视和尊重,请相信:无论怎样强调多态在OOP中的重要性,都不为过。不理解它,也就不会真正明白什么是OOP!

多态的概念与接口重用

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

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

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

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

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

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

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

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

什么是接口重用?

在类中不提供方法的实现,而派生类则必须实现它,即规定了一套接口。凡是含有abstract方法的类被称为“抽象类”,永远无法创建抽象类的实例对象。抽象类是被用来作为接口的。多态的本质就是“将派生类类型的指针赋值给基类类型的指针”(在Object Pascal中是引用),只要这样的赋值发生了,就是在应用多态了,因为在此实行了“向上映射”(“上下”是指类继承层次关系)。

任何想要使通过基类对象指针做到的事情与通过派生类对象指针所做的相同,就要在基类中将这个方法声明为virtual,在派生类中将该方法声明为override。

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

多态的实现与VMT/DMT

从语义上来讲,继承所表现的是“是一种”的关系,也就是说,每个派生类对象必定“是一种”基类对象。所以,任何向基类类型的请求,派生类对象都可以无条件地正常处理。因为直升机“是一种”飞机,喷气式飞机也“是一种”飞机,所以所有对飞机的操作请求,它们都应该可以正常处理。

从语言上来讲,由于派生类通常比基类拥有更多的数据成员而绝对不会更少,派生类对象所占的内存空间必定大于或等于基类对象所占的内存空间。因此,将基类类型的指针指向派生类类型的对象时,在指针的可视范围中的内存必定是可用的,这一部分内存空间必定是属于对象的,所以这种赋值行为是合法的、安全的,并且得到编译器认可的。

当创建一个类的实例之后,编译器会在该对象的内存空间的首4个字节安插一个指针,该指针所指向的地址称为VMT(Virtual Method Table,虚方法表),这个表中存放了该类的所有虚方法的入口地址。在Object Pascal中,所有类实例都会有这么一个指向VMT的指针。如果没有在类中声明虚方法,则该指针为nil。

DMT(动态方法表)

在VMT中可以看到,派生类的虚方法表完全继承了基类的虚方法表,只是将被覆盖了的虚方法的地址改变了。基类和每个派生类都有一份自己的虚方法表。可以想象,随着类层次的扩展,虚方法表将耗费非常大的内存空间。为了防止这种情况,Object Pascal引入了“dynamic”的概念。对于程序员来说,dynamic方法和virtual方法实现相同的功能,只是声明的关键字不同:

Procedure fly(); dynamic; // 是dynamic而不是virtual

被声明为dynamic的方法,其入口地址将被放在DMT中。DMT和VMT的区别在于:对于派生类没有覆盖的方法,这些方法的入口地址不会出现在DMT中,编译器要通过基类的信息来寻找它们的入口地址。

当基类有许多虚方法,而派生类只覆盖很少几个时,区别尤其明显。当派生层次越来越深,派生类数量越来越多,DMT就能节省更多的内存空间。但是DMT中对基类的动态方法的寻址不是直接进行的,因此dynamic方法的寻址比virtual方法要慢许多。

virtual和dynamic的区别仅在于编译器采用不同的晚绑定策略而已,对于程序员来说,它们的功能相同。
如何取舍就看实际的需求了,一般情况下,几乎每个派生类都要覆盖的方法,将它声明为virtual;如果类层次很深,或派生类很多,但某个方法只被很少的派生类覆盖,则将它声明为dynamic。

另外需要注意的是,只有VMT才与C++、COM的vtable兼容,因此当需要这样的兼容性时,只能使用virtual。

小结

传统的说法是,封装、继承、多态是面向对象编程的三个基本特性。实际上,封装只是抽象数据类型(ADT),有了继承才能被称为面向对象。而继承的存在,除了扩展现存类的功能外,另一个更重要的作用就是作为多态存在的基石。

多态是一种能够带来灵活性的东西,它使得通过接口重用来实现代码重用。可以毫不夸张地说,不领会多态,不明白晚绑定,就不可能明白什么是面向对象!因为只有在会用virtual后,才是真正在用面向对象的典范(paradigm)思考……
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值