程序语言多态 - delphi 版本

前言:

所有程序语言都差不多,特写一篇 delphi 版本 的多态;其它语言 类同。

都是一些别人规定的语法而已,别人用一个下午设计一门语言,愚弄天下程序员一生;

不要过于纠结。。。。。。。。


TObject的构造和析构方法

从这说起,先看下定义:

TObject = class
public
constructor Create;
procedure Free;
destructor Destroy; virtual; //这里定义成了虚方法
end; constructor TObject.Create;
begin
end; destructor TObject.Destroy; //Destory是个空方法体,没有具体实现
begin
end; procedure TObject.Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
if Self <> nil then
Destroy;
{$ENDIF}
end;

好的,接下来2个问题:

  1. TObject.Create 为什么没有定义成 虚方法?

    在 Delphi 中,TObject.Create 通常不是定义成虚方法(virtual method)有几个原因:

    1. 构造函数的特殊性:构造函数在 Delphi 中有特殊的语义。当一个对象实例被创建时,它的构造函数会被自动调用。这个过程是在对象内存分配之后、但在对象变量被赋值之前发生的。由于构造函数的这种特殊性,它不能被声明为虚方法。虚方法表(VMT)在对象实例的内存中,而构造函数是在实例内存被完全初始化之前调用的,因此在这个时候虚方法表还不可用。

    2. 继承链中的构造函数调用:在 Delphi 中,如果你在一个派生类的构造函数中想调用其基类的构造函数,你需要使用 inherited Create 语句。这个语句会调用基类中的 Create 构造函数,无论它是否被声明为虚方法。由于 Delphi 的这种机制,将构造函数声明为虚方法没有实际意义,因为派生类总是可以明确地调用基类的构造函数。

    3. 设计模式:在 Delphi 的对象模型中,构造函数通常用于执行那些不依赖于派生类具体实现的初始化代码。因此,将构造函数定义为非虚方法,鼓励开发者在派生类中重写构造函数时,显式地调用基类的构造函数,并确保基类的初始化代码得到执行。

    4. 性能考虑:虚方法调用通常比非虚方法调用稍微慢一些,因为需要通过虚方法表来间接调用方法。虽然在现代计算机上这个性能差异可能微不足道,但在某些性能敏感的代码中,避免不必要的虚方法调用可能仍然是有意义的。

    5. 语义清晰性:将构造函数保持为非虚方法有助于保持代码的语义清晰性。构造函数的主要目的是初始化对象,而不是提供多态行为。多态行为通常通过其他虚方法或属性来实现。

    总的来说,由于 Delphi 对象模型的设计和一些实际考虑,将 TObject.Create 定义为非虚方法是合理的。开发者可以通过在派生类中重写构造函数,并在需要时调用基类的构造函数,来实现所需的初始化逻辑。

  2. TObject.Destroy 为什么被定义成了虚方法 ?

    在 Delphi 和其他使用 Object Pascal 的环境中,TObject.Destroy 被定义为虚方法(virtual method)是因为它支持多态性,这是面向对象编程(OOP)中的一个核心概念。多态性允许派生类(子类)重写基类(父类)中的方法,并且在运行时根据实际对象的类型来决定调用哪个方法。

    TObject.Destroy 作为虚方法,意味着派生类可以覆盖(override)这个方法以提供自定义的销毁逻辑。当调用一个对象的 Destroy 方法时,如果该对象是派生类的实例,并且该派生类提供了自己的 Destroy 方法实现,那么派生类的 Destroy 方法将被调用。这是通过虚方法表(VMT)来实现的,虚方法表是每个对象实例中都有的一个表,用于动态分派虚方法调用。

    这样做有几个好处:

    1. 内存管理:派生类可能需要执行一些特定的清理工作,比如释放动态分配的资源。通过将 Destroy 定义为虚方法,派生类可以确保在对象销毁时执行这些清理工作。

    2. 代码重用:基类中的 Destroy 方法可以包含一些通用的销毁逻辑,而派生类可以通过覆盖这个方法来添加或修改销毁逻辑,同时仍然可以调用基类的 Destroy 方法来执行那些通用的操作。这通过 inherited Destroy 语句来实现。

    3. 对象生命周期的完整性:在对象的生命周期结束时,调用 Destroy 方法可以确保对象以一种可控的方式被销毁。这对于避免内存泄漏和其他资源管理问题非常重要。

    4. 框架和库的灵活性:在一些框架和库中,对象的创建和销毁可能由框架本身来管理。通过将 Destroy 定义为虚方法,这些框架可以确保即使对象的实际类型是未知的,也能正确地销毁对象。

    因此,TObject.Destroy 被定义为虚方法是 Delphi 对象模型中支持多态性和面向对象编程原则的一个重要方面。

所以使用构造方法 和 析构方法的时候,通常这么用,形成一个闭环:

constructor TMyClass.Create;
begin
inherited; //先 调用父亲 Create;
//...
end; destructor TMyClass.Destroy;
begin
//... 先销毁子
inherited;
end;

从哲学的角度讲是这样的;

创建走正序:父亲.Create ----->> 儿子.Create ----->> 孙子.Create

销毁走逆序:父亲.Destroy <<----- 儿子.Destroy <<----- 孙子.Destroy

即:

1.构造方法,如果子类需要加强,或需要重载,那么就需要重定义;

2.析构方法,如果子类需要加强,那么就需要重写覆盖overide父类的,且析构方法一般不重载。

3.普通方法,如果子类需要加强,那么就需要父类定义成虚方法,然后子类覆盖overide;只有这样才能做到多态的情况下使用。

然 以上说的 也是 片面,有些情况下 create还是被定义成了虚方法,比如官方的 TComponent类

  TComponent = class(TPersistent, IInterface, IInterfaceComponentReference)
public
constructor Create(AOwner: TComponent); virtual;
end;

原因又是一堆:

在 Delphi 的 VCL(Visual Component Library)中,TComponent 是许多组件类的基类。TComponent 提供了一个框架,用于处理组件的创建、销毁、拥有关系以及其他一些通用功能。

TComponent 的构造函数 Create(AOwner: TComponent) 被定义为虚方法,主要是出于以下几个原因:

  1. 多态性:虚方法允许派生类重写(override)基类的方法。在 TComponent 的情况下,这意味着派生类可以提供自定义的构造逻辑,同时仍然可以调用基类的构造函数来执行一些通用的初始化代码。

  2. 拥有关系AOwner 参数指定了新创建的组件的拥有者。在 Delphi 中,组件可以拥有其他组件,并负责它们的销毁。通过虚构造函数,派生类可以在创建时建立正确的拥有关系,这对于内存管理和组件的自动销毁至关重要。

  3. 框架灵活性:定义为虚方法使得 Delphi 的框架和库在处理组件时更加灵活。例如,在流(streaming)系统中,当从 DFM 文件创建组件时,框架需要能够动态地调用正确的构造函数来创建组件实例。虚构造函数使得这一过程成为可能,即使组件的实际类型是未知的。

  4. 代码重用和扩展性:允许派生类重写构造函数,意味着开发者可以在不修改基类代码的情况下添加或改变组件的构造行为。这有助于代码的重用和模块化,同时也支持了“开闭原则”(Open/Closed Principle),即对扩展开放,对修改关闭。

  5. Destroy 的一致性:由于 TComponent 的析构函数 Destroy 也是虚方法,将构造函数也定义为虚方法保持了对象创建和销毁过程在多态性方面的一致性。

需要注意的是,尽管构造函数被定义为虚方法,但在 Delphi 中直接调用构造函数的虚方法版本通常是不必要的,也是不推荐的。构造函数的调用通常是通过类类型直接进行的,而不是通过指针或引用进行的动态分派。然而,在框架内部,尤其是在流处理和组件创建的过程中,虚构造函数的间接调用确实会发生。

到此结束 这个话题,知道有这么回事就行了;


虚方法与动态方法

虚方法 的关键字 是 virtual,动态方法的关键字是 dynamic;估计只有 delphi 这么语言里 有这2个关键字;

这2个关键子 都是为了标记 一个方法 可以 被子类重写;由于其他语言 默认 所有方法 都可以被重写,所以不需要这样的关键字;

他们的区别是什么?

在 Delphi 中,虚方法(virtual methods)和动态方法(dynamic methods)是两种不同类型的方法,它们在运行时表现出不同的行为。

  1. 虚方法(Virtual Methods)

    • 虚方法是 Object Pascal 的一种特性,允许派生类(子类)重写基类(父类)的方法。
    • 当一个对象请求调用虚方法时,编译器不知道具体要调用哪个版本的方法(基类版本还是派生类版本),因此它会在运行时通过虚拟方法表(VMT)来查找和调用正确的方法。
    • 所有的虚方法(包括未被覆盖的)在 VMT 中都有入口地址,因此使用虚方法可能会占用更多的内存。
    • 虚方法的调用速度相对较快,因为编译器在编译时就已经为它们生成了特定的代码。
  2. 动态方法(Dynamic Methods)

    • 动态方法也是一种允许在运行时确定调用哪个版本的方法的机制,但它们与虚方法在实现上有所不同。
    • 动态方法不是通过 VMT 来实现的,而是通过动态方法表(DMT)来实现。DMT 是一个存储了动态方法入口地址的表,与 VMT 类似,但仅包含那些被声明为动态的方法。
    • 动态方法的一个主要优势是它们可以节省内存,因为 DMT 只存储那些实际被覆盖的方法的地址,而未被覆盖的方法不会占用额外的空间。
    • 动态方法的调用速度可能略慢于虚方法,因为在运行时可能需要到对象的 DMT 中查找动态方法的地址。

总的来说,虚方法和动态方法都提供了运行时多态性,允许在不知道对象具体类型的情况下调用正确的方法版本。它们的主要区别在于实现机制、内存使用和调用速度上。在实际应用中,开发者可以根据具体需求和性能考虑来选择使用虚方法还是动态方法。

实际开发中,使用虚方法关键字 就可以了,知道是这么回事就行了;

抽象类与抽象方法

抽象方法,在delphi里 又叫 “纯虚/纯动态方法”,抽象方法,父类不实现,只定义,子类需要 overide重写,故抽象方法 一定是虚方法或动态方法,需要与 virtual 和 dynamic 配合使用;国际标准是 一个类一旦含有 抽象方法 这个类要定义成抽象类,抽象类不能自己实例化,需要子类来实例化;这些屁话 你应该知道;

unit Unit4;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs; type
TForm4 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end; /// <summary>
/// 定义一个普通的车类
/// </summary>
TCar = class
function name(): string; virtual; //虚方法父类也是必须实现的,子类可以选择是否覆盖,不必须
end; /// <summary>
/// 定义一个车的抽象类
/// </summary>
TCarAbstract = class abstract
function name(): string; //抽象类与普通的类一样,是可以拥有普通的方法的,但是抽象类不能实例化 仅此而已,其它类有关的功能都有
end; /// <summary>
/// 定义一个普通的车类 + 增加一个纯虚方法 就可以把这个类变成一个抽象类 不能实例化了
/// 间接抽象类
/// </summary>
TCarIndirectAbstract = class
function name(): string; virtual; abstract;//这个是纯虚方法,只定义不实现 纯虚方法也叫做抽象方法;子类没有选择的权利,必须覆盖overide
end; var
Form4: TForm4; implementation {$R *.dfm} { TCar } function TCar.name: string;
begin
//这里必须得实现,否则编译会报错.
Exit('car');
end; { TCarAbstract } function TCarAbstract.name: string;
begin
//抽象类中也可以包含任何剖通方法 与普通类一样。
Exit('CarAbstract');
end; end.

现实中,能用接口解决的,优先使用接口,其次使用 抽象类 抽象方法;

虚方法或抽象方法,举个简单的实例吧,我们通常的需求是这样的,我们定义了一个父类和一个很多子类,我们再使用的时候,只关注用途而不关心具体是哪个类来实现。还有一种情况是是我们事先并不知道是哪个类来实现,需要根据情况动态的判断用哪个类来实现 TStrings,TStream就是典型的应用。见如下demo:

unit Unit5;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type
TForm5 = class(TForm)
Button1: TButton;
Memo1: TMemo;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end; /// <summary>
/// 定义一个汽车基类
/// </summary>
TCar = class
public
function run(): string; virtual;
end; /// <summary>
/// 轿车类
/// </summary>
TJiaoChe = class(TCar)
public
function run(): string; override;
end; /// <summary>
/// 货车类
/// </summary>
THuoChe = class(TCar)
public
function run(): string; override;
end; var
Form5: TForm5; implementation {$R *.dfm} { THuoChe } function THuoChe.run: string;
begin
Result := '货车跑了';
end; { TJiaoChe } function TJiaoChe.run: string;
begin
Result := '轿车跑了';
end; { TCar } function TCar.run: string;
begin
Result := '车跑了';
end; procedure TForm5.Button1Click(Sender: TObject);
var
//我们再使用的时候,往往事先并不知道到底哪个车跑了,或者说车跑了,是根据条件来跑的
c: TCar;
begin
//根据条件来动态的判断哪个车跑了,然后实例化相应的对象
if Trim(Edit1.Text) = '轿车' then
begin
c := TJiaoChe.Create;
Memo1.Lines.Add(c.run);
end else if Trim(Edit1.Text) = '货车' then begin
c := THuoChe.Create;
Memo1.Lines.Add(c.run);
end else begin
c := TCar.Create;
Memo1.Lines.Add(c.run);
end;
c.Free;
end; end.

这样有利于多态的情况下,即定义父类的实例,然后用不同的子类来实现,运行的方法 都是对应的各个子类的。若用多态,即一个父类 + N多子类,且父类的方法需要再子类中加强的情况下,尽量不要用重定义的方法,应该用 virtual + overide的方式;或者说 尽量用 virtual + overide 的方式 而不要用 重定义的方法,这样做的好处是,多态使用的时候,用父类实例去调用方法的时候,实际调用的是各个子类中加强的方法,而不是父类原有的方法。

静态方法

type
TForm2 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
class function aa(): string;
class function bb(): string; static;
function cc(): string; static; //报错
end;

cc报错是因为 静态方法,必须是类方法,那aa和 bb这2个方法 有什么区别呢??

在 Delphi 中,class function 用于声明一个类方法,而类方法又有两种形式:实例化的类方法(通常简称为类方法)和静态类方法。这两种方法在声明和使用上有一些重要的区别。

  1. 实例化的类方法(无 static 关键字)

    • 当类方法没有 static 关键字时,它仍然是类的一个成员,并且可以访问类的类型信息(比如类变量)和其他类方法。
    • 这种类方法在内部可以通过 Self 关键字引用类类型(注意,不是类实例,因为没有“当前实例”的概念)。
    • 实例化的类方法不能访问类的实例变量或实例方法,因为没有与特定对象实例相关联。
    • 尽管它们被称为“实例化的”,但这并不意味着它们需要一个类的实例来调用;相反,这个名字可能有点误导,因为它们是直接在类上调用的,而不是在类的实例上。实际上,更准确的描述可能是“与类关联的方法”。
  2. 静态类方法(带有 static 关键字)

    • 静态类方法完全不依赖于类的实例。它们更像是普通的函数,只是碰巧在类的声明内部定义。
    • 静态类方法不能访问类的实例变量、实例方法或类变量。它们只能访问自己的参数和全局变量。
    • 静态类方法也不能使用 Self 关键字,因为它们不是类的实例成员。
    • 静态类方法通常用于执行与类相关但不依赖于类实例状态的操作。

例子:

type
TMyClass = class
private
class var CV: Integer; // 类变量
public
class function Aa: string; // 实例化的类方法
class function Bb: string; static; // 静态类方法
end; class function TMyClass.Aa: string;
begin
Result := 'This is an instantiated class method.';
// 可以访问类变量 CV
end; class function TMyClass.Bb: string;
begin
Result := 'This is a static class method.';
// 不能访问类变量 CV
end;

在上面的例子中,Aa 是一个实例化的类方法,它可以访问类变量 CV。而 Bb 是一个静态类方法,它不能访问类变量 CV 或任何其他类的成员(除了它自己的参数和全局变量)。

请注意,在 Delphi 的较新版本中(尤其是从 XE3 开始),static 关键字已被弃用,并且静态类方法现在应使用 class static 关键字组合来声明。但是,旧的 static 关键字在现有代码中仍然有效,并且编译器会将其视为 class static 的别名。因此,正确的声明应该是:

class static function Bb: string;

而不是简单地使用 static。这样做有助于清晰地表明该方法是一个静态类方法,而不是一个容易与 C++ 中的静态成员方法混淆的旧式静态方法。

综上 static 关键词 别用了,要用类方法 就直接 class function 开头就行了;

不能被覆盖的方法

//假如要设定 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;

总结overide

切记2点:重写overide 有正反2个作用;

  1. 子类可以继承父类的虚方法,重写;这样子类实例 调用的时候 就是调用子类重写的方法;这个是正向的多态,一般人都理解;

  2. 父类可以调用子类的方法,举例:fu = zi.create;子类又重写了父类的方法,fu这个变量指针引用调用的子类的方法,这个逆向

    的多态,一般人 不容易理解,上游顶层设计者,设计的东西 往往都是 在这里 使用 虚方法;比如我是一个核心组件的设计者,

    允许开发者继承我,并重写我,我调用的时候调用的是开发者的方法,最典型的就是 析构函数 destroy;delphi的核心设计者

    通过调用 free 间接调用了 开发者写的 destroy; overide;方法,从而释放掉了内存;故 destroy方法 不要忘记 加 overide关键字哦;

下班了,不想写了。。。。

程序语言多态(overide) - delphi 版本的更多相关文章

  1. 几种不同程序语言的HMM版本

    几种不同程序语言的HMM版本 “纸上得来终觉浅,绝知此事要躬行”,在继续翻译<HMM学习最佳范例>之前,这里先补充几个不同程序语言实现的HMM版本,主要参考了维基百科.读者有兴趣的话可以研 ...

  2. 《Java从入门到失业》第一章:计算机基础知识(三):程序语言简介

    1.3程序语言简介 我们经常会听到一些名词:低级语言.高级语言.编译型.解释型.面向过程.面向对象等.这些到底是啥意思呢?在正式进入Java世界前,笔者也尝试简单的聊一聊这块东西. 1.3.1低级语言 ...

  3. BT雷人的程序语言

    原文:http://cocre.com/?p=1142  酷壳 这个世界从来都不会缺少另类的东西,人类自然世界如此,计算机世界也一样.编程语言方面,看过本站<6个变态的C语言Hello Worl ...

  4. 【转载】Visual Studio中WinForm窗体程序如何切换.NET Framework版本

    在C#语言的WinForm窗体程序中,有时候我们需要切换WinForm窗体程序项目的.NET Framework版本号,例如从.NET Framework 4.5版本切换到.NET Framework ...

  5. Hybrid App是如何实现网页语言与程序语言的混合?谁占主体?

    [编者按]本文作者@徐珂铭,一位看好Html5的移动互联网的从业人士.喜爱玩技术,会点JAVA.HTML及CSS,有自己的想法及姑且能表达想法的文字,因此有了自己的文章. 基于HTML5的Web Ap ...

  6. atitit.编程语言 程序语言 的 工具性 和 材料性 双重性 and 语言无关性 本质

    atitit.编程语言 程序语言 的 工具性 和 材料性 双重性 and 语言无关性 本质 #---语言的 工具和材料双重性 有的人说语言是个工具,有的人说语言是个材料..实际上语言同时属于两个属性. ...

  7. 数学语言和程序语言的对比:面向过程与面向集合&命题

    共同之处:都使用字符串或数值来引用一个客观实体.当然数字和字符串也可以作为实体对象,这取决于人的解释. 不同之处:数学语句每一行都给出了一个结论, 程序语句的每一行都定义了一个过程.注意这里所指的程序 ...

  8. 情人节红攻瑰--Delphi版本

    在oschina上看到了用c写的红玫瑰, 以前只见过用js写的, 就随手用delphi翻译了c的代码, 效果还不错哈.... 原c作者jokeym贴子 http://www.oschina.net/c ...

  9. 我喜欢的程序语言c++

    我喜欢的程序语言c++我喜欢的程序语言c++

  10. IIS中报错弹出调试,系统日志-错误应用程序名称: w3wp.exe,版本: 8.5.9600.16384,时间戳: 0x5215df96(360主机卫士)

    偶遇一次特殊情况,在使用Web系统导入数据模版(excel)时,服务端IIS会报错并弹出调试框,然后整个网站都处于卡死的debug状态,如果点否不进行调试,则IIS会中断调试,Web系统继续执行,运行 ...

随机推荐

  1. 1、springboot工程新建(单模块)

    系列导航 springBoot项目打jar包 1.springboot工程新建(单模块) 2.springboot创建多模块工程 3.springboot连接数据库 4.SpringBoot连接数据库 ...

  2. 如何使用Markdown编写笔记

    Markdown是什么? Markdown 是一种轻量级标记语言,创始人为约翰·格鲁伯(John Gruber). 它允许人们使用易读易写的纯文本格式编写文档,然后转换成有效的 XHTML(或者HTM ...

  3. Nacos源码 (3) 注册中心

    本文将从一个服务注册示例入手,通过阅读客户端.服务端源码,分析服务注册.服务发现原理. 使用的2.0.2的版本. 客户端 创建NacosNamingService对象 NacosNamingServi ...

  4. 用C# WPF简单实现仪表控件

    时间如流水,只能流去不流回! 点赞再看,养成习惯,这是您给我创作的动力! 本文 Dotnet9 https://dotnet9.com 已收录,站长乐于分享dotnet相关技术,比如Winform.W ...

  5. 【FreeRTOS】堆内存管理

    动态内存分配及其与FreeRTOS的相关性 为了使FreeRTOS更易用,内核对象(如任务.队列.信号量.事件组)不在编译期静态分配,而是在运行时动态分配,FreeRTOS在内核对象创建时分配RAM, ...

  6. JMS微服务开发示例(四)把配置文件appsettings.json 部署在网关,共享给其他相同的微服务

    通常,多个相同的微服务器,它们的appsettings.json配置文件的内容都是一样的,如果,每次修改配置文件,都要逐个替换,那就太繁琐了,我们可以利用网关的文件共享功能,实现配置文件的统一更新. ...

  7. Linux-日期时间-date

  8. [转帖]理解 postgresql.conf 的work_mem 参数配置

    https://developer.aliyun.com/article/401250 简介: 主要是通过具体的实验来理解 work_mem 今天我们着重来了解 postgresql.conf 中的 ...

  9. [转帖]linux系统下grub.cfg详解和实例操作

    linux系统下grub.cfg详解和实例操作 简介 grub是引导操作系统的程序,它会根据自己的配置文件,去引导内核,当内核被加载到内存以后,内核会根据grub配置文件中的配置,找到根分区所使用的文 ...

  10. [转帖]12.JVM运行时数据区之虚拟机栈概述

    https://blog.csdn.net/u011069294/article/details/107050001 目录 1. 内存中的栈与堆 2.栈的优点 1. 内存中的栈与堆 栈是运行时单位,堆 ...