如何强制后代类实现一个抽象函数?

How to force descendant classes to implement an abstract function?

我有一个基本抽象 class,我在其中定义了一个虚拟抽象函数。

TMyBaseClass = class abstract(TForm)
public
  function MyFunction : string; virtual; abstract;
end;

我注意到在编译非抽象后代 class 时没有显示 error/warning。

TMyDescendantClass = class(TMyBaseClass);

目标是逼迫我的同事为每个后代实现功能class。我发现了一个类似的问题 (Delphi 6: Force compiler error on missing abstract class methods?),但解决方案不符合我的目标。

更新1:

类是调用"GetClass"函数得到的

GetClass('TMyDescendantClass').Create

因此,实例创建不会引起警告。

一个有点难看的解决方案是在早期触发一个 runtime 异常,阻止使用它们的后代 classes 除非他们做了实现:

 TMyBaseClass = class 
   public function MyFunction : string; virtual; abstract;
   public procedure AfterConstruction; override;
 end;

....

procedure TMyBaseClass.AfterConstruction; override;
begin
  MyFunction();
  inherited;
end;

前提是函数本身是纯函数(不更改对象状态并且不需要太多对象状态)并且执行成本低廉。

一些优化可能还包括仅针对第一次实例化的一次性测试,并将其仅绑定到调试版本。

 TMyBaseClass = class 
   public function MyFunction : string; virtual; abstract;
   public procedure AfterConstruction; override;
   private class var MyFunction_tested: Boolean;
 end;

....

procedure TMyBaseClass.AfterConstruction; override;
begin
  {$IfOpt D+}
  if not MyFunction_tested then begin
    MyFunction();
    MyFunction_tested := true; 
  end;
  {$EndIf}
  inherited;
end;

更新。

Classes are obtained by calling "GetClass" function. GetClass('TMyDescendantClass').Create

因此,您使用后期绑定,其中程序核心(和编译器)实际上不知道第三方开发人员制作的插件会扩展哪些 classes。诚然,实际上您可能有一个固定的插件列表和一个固定的开发人员列表,但是对于程序结构来说没有区别。您的程序在编译阶段还没有定义,只有特定的运行时插件集才能完全定义它。

这意味着你在编译你的核心程序时真的不能全部测试。以后任何人都可以实现一个全新的插件并将其插入。您的 Delphi 无法预见未来。所以你必须求助于运行时检查。这就是基于插件的类乐高应用程序的工作方式。

现在的问题是如何安排运行时检查,所以它会尽可能 "greedy"(总是失败 => 早期失败:在内部测试阶段,而不是部署后几个月)同时它会消耗尽可能少的运行时资源。

实际上,检查一个且仅一个特定功能本身似乎是有问题的想法。使用 RTTI,您可以检查是否仍然存在任何抽象函数。 诚然,使用 RTTI 相对较慢,但我认为从 HDD 加载 BPL 文件无论如何都会变慢,所以这里无关紧要。

我个人认为您应该将两个不同的操作分开 - 获取 class 并实例化它。就像在 Java 和 DotNet 运行时中所做的一样,在这些操作之间有专用的 class checkers/verifiers。

  TmpClass := GetClass('TMyDescendantClass');
  if not TmpClass.InheritsFrom( TMyBaseForm ) then 
     raise EPluginSystemMisconfiguration.Create(.....); 
  MyPluginFormClass := TMyBaseForm( TmpClass ); 
  VerifyPluginClass( MyPluginFormClass ); // immediately raises exception if class is not valid
  MyPluginForm := MyPluginFormClass.create(Application);

应该VerifyPluginClass实施什么检查取决于您。但是其中一项检查应该是 class 是否具有任何抽象的未实现函数。

How i can determine if an abstract method is implemented?

如果可能,请尝试建立定期单元测试或集成测试的实践 - 然后非常 VerifyPluginClass 子例程将在测试框架中重用,让您的合作开发人员能够抓住他们的这种错误本身。

声明一个包含抽象方法的class没有问题,这里不发出警告;如果您创建抽象 class 的实例并且 Delphi 编译器发出相应的警告,则会出现问题。例如,

var
  Obj: TMyDescendantClass;

begin
  Obj:= TMyDescendantClass.Create;
  ...

导致警告

[DCC Warning]: W1020 Constructing instance of 'TMyDescendantClass' containing abstract method 'TMyBaseClass.MyFunction'

如果您坚持当前的设计,那么除了在运行时等待错误之外,您无能为力。

如果您静态引用 classes,那么如果您实例化包含抽象方法的 classes,您会看到警告。但是因为你是动态获取的classes,所以编译器帮不了你。

向 class 添加接口不会有多大用处。假设您更改了 class 以便它实现了一个接口?您必须在基础 class 中实现它。你会怎么做?通过使用虚拟方法?你会回到你开始的地方。

假设您可以以某种方式强制派生 classes 的实现者实现所有这些抽象方法。是什么阻止他们这样做:

type
  TMyDescendantClass = class(TMyBaseClass)
  public
    function MyFunction: string; override;
  end;

function TMyDescendantClass.MyFunction: string;
begin
  // TODO: implement this method
end;

有时合同无法在编译时执行!


而不是使用 classes 的抽象方法,您可以完全改变策略。不要要求实现者从基础 class 派生。请他们在您请求时为您提供接口。这让他们承担了责任。他们必须实现接口(并实例化它)。然后他们有义务实现每个功能。当然,这并不是说它会阻止他们错误地实现功能!

根据只有您知道的因素,这可能不如您当前的设计方便。但只有你能决定。

如果后代 classes 被声明为密封的,那么编译器会为任何未实现的抽象过程和函数引发错误。所以以下将在编译时引发错误:

TMyBaseClass = class abstract(TForm)
public
  function MyFunction : string; virtual; abstract;
end;

TMyDescendantClass = class sealed(TMyBaseClass);

当然你不能强迫你的同事声明 class 是密封的,但也许它会有所帮助。

一个小提示,如何强制生成更好的代码:使用 -W^ 作为额外的编译器指令。

它会将所有警告视为错误,如果不实现抽象方法,您将无法编译代码。 你必须使用 virtual,而不是 dynamic 指令才能工作(从 Delphi 10.4 开始)。由于您使用了后期绑定,指令将无济于事,但它肯定会修复代码中的警告,迫使每个人都修复它们。