【问题标题】:Delphi: overridden method not called for objects instantiated using RTTIDelphi:不为使用 RTTI 实例化的对象调用重写的方法
【发布时间】:2011-12-28 09:59:39
【问题描述】:

我正在尝试在 D2010 中使用 RTTI 克隆对象。到目前为止,这是我的尝试:

uses SysUtils, TypInfo, rtti;
type
  TPerson = class(TObject)
  public
    Name: string;
    destructor Destroy(); Override;
  end;
destructor TPerson.Destroy;
begin
  WriteLn('A TPerson was freed.');
  inherited;
end;
procedure CloneInstance(SourceInstance: TObject; DestinationInstance: TObject; Context: TRttiContext); Overload;
var
  rSourceType:      TRttiType;
  rDestinationType: TRttiType;
  rField:           TRttiField;
  rSourceValue:     TValue;
  Destination:      TObject;
  rMethod:          TRttiMethod;
begin
  rSourceType := Context.GetType(SourceInstance.ClassInfo);
  if (DestinationInstance = nil) then begin
    rMethod := rSourceType.GetMethod('Create');
    DestinationInstance := rMethod.Invoke(rSourceType.AsInstance.MetaclassType, []).AsObject;
  end;
  for rField in rSourceType.GetFields do begin
    if (rField.FieldType.TypeKind = tkClass) then begin
      // TODO: Recursive clone
    end else begin
      // Non-class values are copied (NOTE: will cause problems with records etc.)
      rField.SetValue(DestinationInstance, rField.GetValue(SourceInstance));
    end;
  end;
end;
procedure CloneInstance(SourceInstance: TObject; DestinationInstance: TObject); Overload;
var
  rContext:       TRttiContext;
begin
  rContext := TRttiContext.Create();
  CloneInstance(SourceInstance, DestinationInstance, rContext);
  rContext.Free();
end;
var
  Original:     TPerson;
  Clone:        TPerson;
begin
  ReportMemoryLeaksOnShutdown := true;
  Original := TPerson.Create();
  CloneInstance(Original, Clone);
  Clone.Free();
  Original.Free();
  ReadLn;
end.

有点令人失望的是,我没有看到不止一次出现“一个 TPerson 被释放了。”到输出(通过单步执行程序来确认) - 使用重写的析构函数只销毁原始文件。

谁能帮我调用被覆盖的析构函数? (也许可以解释为什么一开始就没有调用它。)谢谢!

【问题讨论】:

    标签: delphi delphi-2010 rtti


    【解决方案1】:

    你的代码有几个问题。

    您没有将 Clone 变量初始化为 nil。这在我的机器上导致了上层 CloneInstance 方法中的访问冲突,因为没有创建克隆,因为传入的值不是 nil。

    您没有将 DestinationInstance 参数声明为 var。这意味着上层 CloneInstance 方法中的实例化不会返回给调用者。在参数中添加var 即可解决问题。您确实需要在从程序的 main 方法调用 CloneInstance 时使用TObject(Clone),否则 Delphi 会抱怨“没有可以使用这些参数调用的重载方法”。这是因为 var 参数希望将其声明的确切类型传递给它们。

    我将您的代码更改为:

    uses
      SysUtils,
      TypInfo,
      rtti;
    
    type
      TPerson = class(TObject)
      public
        Name: string;
        constructor Create;
        destructor Destroy(); Override;
      end;
    
    constructor TPerson.Create;
    begin
      WriteLn('A TPerson was created');
    end;
    
    destructor TPerson.Destroy;
    begin
      WriteLn('A TPerson was freed.');
      inherited;
    end;
    
    procedure CloneInstance(SourceInstance: TObject; var DestinationInstance: TObject; Context: TRttiContext); Overload;
    var
      rSourceType:      TRttiType;
      rDestinationType: TRttiType;
      rField:           TRttiField;
      rSourceValue:     TValue;
      Destination:      TObject;
      rMethod:          TRttiMethod;
    begin
      rSourceType := Context.GetType(SourceInstance.ClassInfo);
      if (DestinationInstance = nil) then begin
        rMethod := rSourceType.GetMethod('Create');
        DestinationInstance := rMethod.Invoke(rSourceType.AsInstance.MetaclassType, []).AsObject;
      end;
      for rField in rSourceType.GetFields do begin
        if (rField.FieldType.TypeKind = tkClass) then begin
          // TODO: Recursive clone
        end else begin
          // Non-class values are copied (NOTE: will cause problems with records etc.)
          rField.SetValue(DestinationInstance, rField.GetValue(SourceInstance));
        end;
      end;
    end;
    
    procedure CloneInstance(SourceInstance: TObject; var DestinationInstance: TObject); Overload;
    var
      rContext:       TRttiContext;
    begin
      rContext := TRttiContext.Create();
      CloneInstance(SourceInstance, DestinationInstance, rContext);
      rContext.Free();
    end;
    
    var
      Original:     TPerson;
      Clone:        TPerson;
    begin
      Clone := nil;
      ReportMemoryLeaksOnShutdown := true;
      Original := TPerson.Create();
      Original.Name := 'Marjan';
    
      CloneInstance(Original, TObject(Clone));
      Original.Name := 'Original';
      WriteLn('Original name: ', Original.Name);
      WriteLn('Clone name: ', Clone.Name);
    
      Clone.Free();
      Original.Free();
      ReadLn;
    end.
    

    我添加了一个构造函数来查看正在创建的两个实例,并添加了几行代码来检查克隆后的名称。输出内容为:

    A TPerson was created
    A TPerson was created
    Original name: Original
    Clone name: Marjan
    A TPerson was freed.
    A TPerson was freed.
    

    【讨论】:

    • 非常感谢!它就像一个魅力。并引用荷马辛普森的话:D'oh! (这不是我的第一次,但希望是最后一次,被丢失的“var”咬伤)。
    • 哦,附带说明:即使 Clone 未明确设置为 nil,我也没有获得 AV。
    • @conciliator:好吧,不明确设置它意味着你不知道它的值是什么,它可能是 nil,它可能是任何东西。这意味着您的代码可能会出现不可预测的行为,因为它将取决于之前 Clone 地址中的值。而且,AV 并不总是立即发生。如果您只调用不使用类的任何字段的方法,那么您可能会好很多年!然后,当它确实从背后咬你时,你将很难弄清楚它是从哪里来的。 TLDR:总是初始化你的变量。
    • 你说得对,应该养成初始化所有变量的习惯。再次感谢。
    【解决方案2】:

    一个示例解决方案(用于构造函数,但在这种情况下基本上也可用)位于

    How can I create an Delphi object from a class reference and ensure constructor execution? in this answer

    但是它需要知道目标类型...这可能不是一个选项

    【讨论】:

    • OP 似乎在做与链接答案(变体 2)完全相同的事情,尽管是在两个语句而不是一个语句中。你真的需要像链接答案中的 Create 一样调用 Destroy 吗?我当然希望不会。一旦一个类被正确实例化,调用该实例的对象引用上的任何方法都应该遵循正常规则。 (并且在实例化之后使用强制转换,尽管您是对的,在正确类型的变量上调用了 Destroy (ugh))。
    猜你喜欢
    • 2011-03-05
    • 1970-01-01
    • 2010-11-26
    • 2010-09-15
    • 2011-01-06
    • 2020-05-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多