【问题标题】:Cast an object to parent class in Delphi在 Delphi 中将对象强制转换为父类
【发布时间】:2012-10-05 09:02:22
【问题描述】:

给定以下类层次结构:

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 过程必须位于类层次结构之外。我无法反转结构,因此无法利用类继承和多态性。
此外,这只是一个例子。我希望得到专注于核心问题的答案:如何在运行时将对象强制转换为其父类

【问题讨论】:

  • 在我看来你的设计是错误的,你不应该与标准的 OOP 功能作斗争......
  • @whosrdaddy 它可能正在使用访问者设计模式(出于其他充分理由)。好的设计通常是在相互冲突的需求之间进行权衡。

标签: delphi class parent


【解决方案1】:

在这种情况下,DoSomething 过程必须位于类层次结构之外。我无法反转结构,因此无法利用类继承和多态性。 此外,这只是一个例子。我想专注于核心问题:如何在运行时将对象强制转换为其父类。

理解这一点的关键是 Delphi 是一种静态类型语言。还要记住,您正在调用非多态过程。这意味着参数的类型是在编译时确定的。重载决议基于该类型。因此,重载决议发生在编译时。

所以,你的例子:

DoSomething(TClass1(AObj))

做你想做的,因为参数的类型在编译时是已知的。根本不可能做出类似的东西

DoSomething(AObj.ClassParent(AObj))

做你想做的,因为参数的类型必须在编译时知道。

如何在运行时将对象强制转换为其父类?

这是问题的症结所在。强制转换不是运行时构造,而是编译时构造。所以简单的答案是您不能将对象强制转换为它的运行时类型。

如果您不能使用多态调度,那么您唯一的选择是硬编码强制转换。 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

显然,这种方法依赖于您遵循命名约定。

与硬编码转换变体相比,这种方法的主要优点之一是调用继承方法的顺序由类层次结构决定。

【讨论】:

  • 谢谢大卫,我更新了问题以更好地解释上下文
  • 没有那些强大的 RTTI 东西? :-)
  • RTTI 代表运行时类型信息。正如我所解释的,重载方法解析发生在编译时。 RTTI 帮不了你。
  • 啊。你可以用 RTTI 做的是使用 RTTI 实现调度程序。我将编造一个简单的例子。
  • 如果我的 DoSomething_Txxx 程序需要更多参数怎么办?例如DoSomething_Txxx(AObj : Txxx; Asl : TStringList)。我应该如何使用 LMethod.Invoke?
【解决方案2】:

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 类型参数的重载过程,但您忘记在 Dispatcher 中删除带有 TClass2() 类型的调用,您将得到两个调用采用TClass1 参数的重载方法,因为这将是现在使用的最佳匹配:

DoSomething(TClass2(Obj))

【讨论】:

  • 我可以将 Dispatcher 定义为 procedure Dispatcher(Obj: Tobject),并将 AsClass 定义为局部变量,其中 AsClass := Obj.ClassType?这会起作用吗,或者 AsClass 将永远是 TObject?
  • 是的,你可以这样做。我不太清楚为什么 Cosmin 还没有这样做。
  • 我已经进行了编辑,但第一个变体(带有参数)也有一些优点。在正常的 OOP 中,每个被覆盖的方法都可以选择是否调用 inherited。如果你想允许,那么你需要恢复到带有参数的方法,删除递归代码并将类似这样的内容添加到你的DoSomething 过程中:Dispatcher(Obj, TClass3.ClassParent),其中TClass3 确实是硬编码的并且是类型参数。
  • 我接受了这个解决方案,因为它澄清了我在这个上下文中的编译器和链接器角色。另外,因为它引入了 Dispatcher 解决方案,对我的需求非常有用。感谢 Cosmin,感谢 David 的想法和耐心
【解决方案3】:

还没有人提到虚拟类方法。尽管对您的问题的字面答案仍然是“不,这是不可能的”,但您可以编写这样的代码:

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;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-06-10
    • 1970-01-01
    • 2012-05-30
    • 2011-09-26
    • 1970-01-01
    • 1970-01-01
    • 2014-01-16
    相关资源
    最近更新 更多