如何强制后代类实现一个抽象函数?
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 开始)。由于您使用了后期绑定,指令将无济于事,但它肯定会修复代码中的警告,迫使每个人都修复它们。
我有一个基本抽象 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 开始)。由于您使用了后期绑定,指令将无济于事,但它肯定会修复代码中的警告,迫使每个人都修复它们。