程序语言多态(overide) - delphi 版本

前言:

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

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

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


TObject的构造和析构方法

从这说起,先看下定义:

pascal 复制代码
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个问题:

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 对象模型中支持多态性和面向对象编程原则的一个重要方面。

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

pascal 复制代码
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类

pascal 复制代码
  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 配合使用;国际标准是 一个类一旦含有 抽象方法 这个类要定义成抽象类,抽象类不能自己实例化,需要子类来实例化;这些屁话 你应该知道;

pascal 复制代码
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:

pascal 复制代码
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 的方式 而不要用 重定义的方法,这样做的好处是,多态使用的时候,用父类实例去调用方法的时候,实际调用的是各个子类中加强的方法,而不是父类原有的方法。

静态方法

pascal 复制代码
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 关键字,因为它们不是类的实例成员。
    • 静态类方法通常用于执行与类相关但不依赖于类实例状态的操作。

例子:

pascal 复制代码
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 的别名。因此,正确的声明应该是:

delphi 复制代码
class static function Bb: string;

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

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

不能被覆盖的方法

pascal 复制代码
//假如要设定 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;

不能被继承的类

pascal 复制代码
//用 class sealed 是不能被继承的
TMyClass = class sealed(TObject)
//...
end;

总结overide

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

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

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

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

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

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

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