给定以下类层次结构:
TClass1 = class
end;
TClass2a = class(TClass1)
end;
TClass2b = class(TClass1)
end;
我使用以下重载程序对它们进行操作
procedure DoSomething(AObj : TClass1); overload;
begin
// ...Do something for TClass1
end
procedure DoSomething(AObj : TClass2a); overload;
begin
// Do something as parent class
DoSomething(TClass1(AObj))
// ...Do something for TClass2a
end
procedure DoSomething(AObj : TClass2b); overload;
begin
// Do something as parent class
DoSomething(TClass1(AObj))
// ...Do something for TClass2b
end
如何动态地将每个参数转换为其父类,而不是硬编码呢?
我想替换这个:
// Do something as parent class
DoSomething(TClass1(AObj))
有更通用的东西,比如这个
// Do something as parent class
DoSomething(AObj.ClassParent(AObj))
更新:在这种情况下,DoSomething过程必须位于类层次结构之外。我不能反转结构,所以我不能利用类继承和多态
此外,这只是一个例子。我想回答关注核心问题:如何在运行时将对象转换为其父类。
答案 0 :(得分:6)
在这种情况下,DoSomething过程必须位于类层次结构之外。我不能反转结构,所以我不能利用类继承和多态。 此外,这只是一个例子。我想关注核心问题:如何在运行时将对象强制转换为父类。
理解这一点的关键是Delphi是一种静态类型语言。还要记住,您正在调用非多态过程。这意味着参数的类型是在编译时确定的。并且重载决策基于该类型。因此,重载解析在编译时发生。
所以,你的例子:
DoSomething(TClass1(AObj))
执行您想要的操作,因为参数的类型在编译时是已知的。根本不可能做出像
这样的事情DoSomething(AObj.ClassParent(AObj))
执行您想要的操作,因为必须在编译时知道参数的类型。
如何在运行时将对象强制转换为父类?
这是问题的症结所在。 Casting不是运行时构造,它是一个编译时构造。所以简单的答案就是你不能将对象强制转换为它的运行时类型。
如果您不能使用多态分派,那么您唯一的选择是硬编码的强制转换。 Cosmin的答案中的例子说明了如何以一种非常有用的方式做到这一点,但事实仍然是在编译时解决了重载。根本没有办法逃避这一点。
您在评论中询问RTTI是否有用。好吧,它已经无法帮助您解决已经讨论过的任何转换或重载问题。但是,它可以帮助您避免许多样板硬编码演员表。这是一个简单的例子:
program Project1;
{$APPTYPE CONSOLE}
uses
System.TypInfo,System.Rtti;
type
TClass1 = class
end;
TClass1Class = class of TClass1;
TClass2a = class(TClass1)
end;
TClass2b = class(TClass1)
end;
type
TClass1Dispatcher = class
private
class var Context: TRttiContext;
public
class procedure DoSomething_TClass1(AObj: TClass1);
class procedure DoSomething_TClass2a(AObj: TClass2a);
class procedure DoSomething_TClass2b(AObj: TClass2b);
class procedure DoSomething(AObj: TClass1; AClass: TClass1Class); overload;
class procedure DoSomething(AObj: TClass1); overload;
end;
class procedure TClass1Dispatcher.DoSomething_TClass1(AObj: TClass1);
begin
Writeln('DoSomething_TClass1');
end;
class procedure TClass1Dispatcher.DoSomething_TClass2a(AObj: TClass2a);
begin
Writeln('DoSomething_TClass2a');
end;
class procedure TClass1Dispatcher.DoSomething_TClass2b(AObj: TClass2b);
begin
Writeln('DoSomething_TClass2b');
end;
class procedure TClass1Dispatcher.DoSomething(AObj: TClass1; AClass: TClass1Class);
var
LType: TRttiType;
LMethod: TRttiMethod;
begin
if AClass<>TClass1 then
DoSomething(AObj, TClass1Class(AClass.ClassParent));
LType := Context.GetType(TypeInfo(TClass1Dispatcher));
LMethod := LType.GetMethod('DoSomething_'+AClass.ClassName);
LMethod.Invoke(Self, [AObj]);
end;
class procedure TClass1Dispatcher.DoSomething(AObj: TClass1);
begin
DoSomething(AObj, TClass1Class(AObj.ClassType));
end;
begin
TClass1Dispatcher.DoSomething(TClass1.Create);
TClass1Dispatcher.DoSomething(TClass2a.Create);
TClass1Dispatcher.DoSomething(TClass2b.Create);
Readln;
end.
输出继电器:
DoSomething_TClass1 DoSomething_TClass1 DoSomething_TClass2a DoSomething_TClass1 DoSomething_TClass2b
显然,这种方法依赖于您遵循命名约定。
这种方法相对于硬编码的转换变体的主要好处之一是调用继承方法的顺序由类层次结构决定。
答案 1 :(得分:3)
overload
仅在编译时有用,它允许编译器根据作为参数传递的对象的类型选择最合适的方法。您不能在运行时使用overload
机制来动态地进行调用,因为到那时,代码已经编译并且已经选择了一个重载过程。大家都知道调用正确的方法(基于“重载”逻辑)可能甚至不可用:如果编译器从未选择重载方法,则链接器可能已丢弃。因此,您无法使用RTTI,因为该方法可能只是在可执行文件中不存在,除非您已经对其进行了硬编码。
您唯一的选择是做一些硬编码。我创建了一个方法,它接受两个参数,操作对象和TClass
参数,如下所示:
procedure Dispatcher(Obj: TClass1);
var AsClass: TClass;
begin
AsClass := Obj.ClassType;
while Assigned(AsClass) do
begin
// Hard-coded dispatch for the type in AsClass.
if AsClass.InheritsFrom(TClass3) then
DoSomething(TClass3(Obj))
else if AsClass.InheritsFrom(TClass2) then
DoSomething(TClass2(Obj))
else if AsClass.InheritsFrom(TClass1) then
DoSomething(TClass1(Obj));
// This emulates the "inherited" call in normal polymorphic OOP.
// We're simply recursively calling the dispatcher for the parent of AsClass.
AsClass := AsClass.ClassParent;
end;
end;
给定TClass3
类型的对象,此过程将调用DoSomething
3次,每次继承一次。并且由于硬编码演员,它将选择适当的重载版本。
示例代码:
var X1: TClass1;
begin
X1 := TClass3.Create;
Dispatcher(X1); // This will call all 3 versions of DoSomething, in order.
end;
由于代码并没有真正使用overloaded
关键字作为有用的东西,我会放弃它的使用,为所有方法赋予不同的名称,因此Dispatcher方法中的代码如下所示:
procedure Dispatcher(Obj: Tobject);
var AsClass: TClass;
begin
AsClass := Obj.ClassType;
if AsClass.InheritsFrom(TClass3) then
DoSomething_Class3(TClass3(Obj))
else if AsClass.InheritsFrom(TClass2) then
DoSomething_Class2(TClass2(Obj))
else if AsClass.InheritsFrom(TClass1) then
DoSomething_Class1(TClass1(Obj));
if AsClass.ClassParent <> nil then
Dispatcher(Obj, AsClass.ClassParent);
end;
从长远来看,这种变体更安全,因为它不依赖于编译器魔法。例如,在第一个变体中,如果您决定删除适用于TClass2
类型参数的重载过程,但是您忘记在调度程序中使用TClass2()
强制转换掉掉调用,那么为获取TClass1
参数的重载方法调用两次,因为这将是现在用户的最佳匹配:
DoSomething(TClass2(Obj))
答案 2 :(得分:2)
没人提到虚拟类方法。虽然你的问题的字面答案仍然是“不,这是不可能的”,你可以编写这样的代码:
type
TClass1 = class
end;
TClass2a = class(TClass1)
end;
TClass2b = class(TClass1)
end;
type
TSomething1 = class
class procedure DoSomething(AObj : TClass1); virtual;
end;
TSomething2a = class(TSomething1)
class procedure DoSomething(AObj : TClass1); override;
end;
TSomething2b = class(TSomething1)
class procedure DoSomething(AObj : TClass1); override;
end;
{ TSomething1 }
class procedure TSomething1.DoSomething(AObj: TClass1);
begin
ShowMessage(AObj.ClassName);
end;
{ TSomething2a }
class procedure TSomething2a.DoSomething(AObj: TClass1);
begin
inherited;
ShowMessage('2A');
end;
{ TSomething2b }
class procedure TSomething2b.DoSomething(AObj: TClass1);
begin
inherited;
ShowMessage('2B');
end;