如何在动态创建对象时使Delphi调用正确的构造函数?

huangapple go评论62阅读模式
英文:

How to make Delphi call correct constructor during dynamic creating?

问题

我有问题,似乎在动态创建时调用了不正确的构造函数。

我几乎与5年前提出了完全相同的问题(https://stackoverflow.com/questions/44165088/why-does-delphi-call-incorrect-constructor-during-dynamic-object-creation),我已经查看过了。但那个线程有覆盖虚拟调用的问题,我现在没有。我还尝试在StackOverflow上搜索匹配的问题,但找不到答案。

我正在处理遗留代码,所以我没有写这个代码的大部分部分。(如果你在下面看到带有"//kt"的注释,那就是我)

代码有一个基类TPCEItem,如下所示。请注意,它没有构造函数。

  TPCEItem = class(TObject)
  {用于PCE项目的基类}
  private
    // 不相关的内容
  public
    // 不相关的内容
  end;

接下来,有一个用于传递参数的类类型。

  TPCEItemClass = class of TPCEItem;

接下来,有一个子类,如下所示。请注意,它有一个构造函数。编译器不允许我在这个Create方法中添加"override",因为声明这个方法的祖先类(TObject)没有将其定义为虚方法。

  TPCEProc = class(TPCEItem)
  {用于过程的类}
  protected
    // 不相关的内容
  public
    // 不相关的内容
    constructor Create;
    destructor Destroy; override;
  end;

然后,代码有一个用于复制数据的函数,这是各种派生类型的综合体。由于这是旧代码,大多数这些列表都是普通的TListTStringList,保存着无类型的指针。因此,为了正确使用每个复制命令,传递了相应的类型。

procedure TPCEData.CopyPCEData(Dest: TPCEData);
begin
  Dest.Clear;

  // 不相关的内容

  CopyPCEItems(FVisitTypesList,  Dest.FVisitTypesList,  TPCEProc); //kt添加
  CopyPCEItems(FDiagnoses,       Dest.FDiagnoses,       TPCEDiag);
  CopyPCEItems(FProcedures,      Dest.FProcedures,      TPCEProc);
  CopyPCEItems(FImmunizations,   Dest.FImmunizations,   TPCEImm);
  CopyPCEItems(FSkinTests,       Dest.FSkinTests,       TPCESkin);
  CopyPCEItems(FPatientEds,      Dest.FPatientEds,      TPCEPat);
  CopyPCEItems(FHealthFactors,   Dest.FHealthFactors,   TPCEHealth);
  CopyPCEItems(FExams,           Dest.FExams,           TPCEExams);

  // 不相关的内容
end;

CopyPCEItems如下所示:

procedure TPCEData.CopyPCEItems(Src: TList; Dest: TObject; ItemClass: TPCEItemClass);
var
  AItem: TPCEItem;
  i: Integer;
  IsStrings: boolean;
  Obj : TObject;
begin
  if (Dest is TStrings) then begin
    IsStrings := TRUE
  end else if (Dest is TList) then begin
    IsStrings := FALSE
  end else begin
    exit;
  end;
  for i := 0 to Src.Count - 1 do begin
    Obj := TObject(Src[i]);
    if(not TPCEItem(Src[i]).FDelete) then begin
      AItem := ItemClass.Create;  //<--- 有问题的行
      if (Obj.ClassType = TPCEProc) and (ItemClass = TPCEProc) then begin  //kt添加的if块和下面的子块
        TPCEProc(Obj).CopyProc(TPCEProc(AItem));
      end else begin
        AItem.Assign(TPCEItem(Src[i]));  //kt <-- 最初这一行是独立的。
      end;
      if (IsStrings) then begin
        TStrings(Dest).AddObject(AItem.ItemStr, AItem)
      end else begin
        TList(Dest).Add(AItem);
      end;
    end;
  end;
end;

有问题的行是下面这一行:

AItem := ItemClass.Create;

当我使用调试器逐步执行代码并停在这一行时,对变量ItemClass的检查结果如下:

ItemClass  = TPCEProc

问题在于.Create调用的是TObject.Create,而不是TPCEProc.Create,这不允许我实例化一些必需的TStringList,后来导致了访问冲突错误。

有人能帮助我理解这里发生了什么吗?我怀疑问题出在这一行:

TPCEItemClass = class of TPCEItem;

是因为这是一个祖先类型(即TPCEItem)的类类型,所以它不能正确传递子类型(TPCEProc)的信息吗?但如果是这样,为什么调试器显示ItemClass = TPCEProc

我在Delphi中编程已经至少30年了,但我仍然对多态性感到困惑。我已经反复阅读相关内容,但总是碰到难题。

提前感谢您的帮助。

英文:

I'm having problems with my Delphi 2006 seeming to call the incorrect constructor during dynamic creation.

I asked almost the exact same question 5 yrs ago (https://stackoverflow.com/questions/44165088/why-does-delphi-call-incorrect-constructor-during-dynamic-object-creation), and I have reviewed that. But that thread had issues of overriding virtual calls which I don't have now. I have also tried searching through StackOverflow for a matching question, but couldn't find an answer.

I am working with legacy code, so I didn't write much of this. (If you see comments below with '//kt' adding something, that is me).

The code has base class, TPCEItem as follow. Note that it does NOT have a constructor.

  TPCEItem = class(TObject)
  {base class for PCE items}
  private
    <irrelevent stuff>
  public
    <irrelevent stuff>
  end;

Next, there is class type to use for passing a parameter (more below).

  TPCEItemClass = class of TPCEItem;

Next I have a child class as follows. Note that it DOES have a contructor. The compiler will not allow me to add 'override' to this create method because the ancestor class where this is declared (TObject) does not define it as virtual.

  TPCEProc = class(TPCEItem)
  {class for procedures}
  protected
    <irrelevent stuff>
  public
    <irrelevent stuff>
    constructor Create;
    destructor Destroy; override;
  end;

The code then has a function for copying data, which is a conglomeration of descendant types. Because this is older code, mosts of these lists are plain TLists or TStringLists, holding untyped pointers. Thus for each copy command a corresponding type is passed in for correct use.

procedure TPCEData.CopyPCEData(Dest: TPCEData);
begin
  Dest.Clear;

  <irrelevent stuff>

  CopyPCEItems(FVisitTypesList,  Dest.FVisitTypesList,  TPCEProc); //kt added
  CopyPCEItems(FDiagnoses,       Dest.FDiagnoses,       TPCEDiag);
  CopyPCEItems(FProcedures,      Dest.FProcedures,      TPCEProc);
  CopyPCEItems(FImmunizations,   Dest.FImmunizations,   TPCEImm);
  CopyPCEItems(FSkinTests,       Dest.FSkinTests,       TPCESkin);
  CopyPCEItems(FPatientEds,      Dest.FPatientEds,      TPCEPat);
  CopyPCEItems(FHealthFactors,   Dest.FHealthFactors,   TPCEHealth);
  CopyPCEItems(FExams,           Dest.FExams,           TPCEExams);

  <irrelevent stuff>
end;

This CopyPCEItems is as follows:

procedure TPCEData.CopyPCEItems(Src: TList; Dest: TObject; ItemClass: TPCEItemClass);
var
  AItem: TPCEItem;
  i: Integer;
  IsStrings: boolean;
  Obj : TObject;

begin
  if (Dest is TStrings) then begin
    IsStrings := TRUE
  end else if (Dest is TList) then begin
    IsStrings := FALSE
  end else begin
    exit;
  end;
  for i := 0 to Src.Count - 1 do begin
    Obj := TObject(Src[i]);
    if(not TPCEItem(Src[i]).FDelete) then begin
      AItem := ItemClass.Create;  //<--- THE PROBLEMATIC LINE
      if (Obj.ClassType = TPCEProc) and (ItemClass = TPCEProc) then begin  //kt added if block and sub block below
        TPCEProc(Obj).CopyProc(TPCEProc(AItem));
      end else begin
        AItem.Assign(TPCEItem(Src[i]));  //kt <-- originally this line was by itself.
      end;
      if (IsStrings) then begin
        TStrings(Dest).AddObject(AItem.ItemStr, AItem)
      end else begin
        TList(Dest).Add(AItem);
      end;
    end;
  end;
end;

The problematic line is as below:

      AItem := ItemClass.Create;

When I step through the code with the debugger, and stop on this line, an inspection of the variable ItemClass is as follows

  ItemClass  = TPCEProc

The problems is that the .Create is calling TObject.Create, not TPCEProc.Create, which doesn't give me an opportunity to instantiate some needed TStringLists, and later leads to access violation error.

Can anyone help me understand what is going on here? I have a suspicion that the problem is with this line:

TPCEItemClass = class of TPCEItem;

It is because this is of a class of an ancestor type (i.e. TPCEItem), that it doesn't properly carry the information for the child type (TPCEProc)?? But if this is true, then why does the debugger show that ItemClass = TPCEProc??

How can I effect a call to TPCEProc.Create?

I have been programming in Delphi for at least 30 yrs, and it frustrates me that I keep having problems with polymorphism. I have read about this repeatedly. But I keep hitting walls.

Thanks in advance.

答案1

得分: 2

当您通过元类构建对象时,需要将其基类构造函数标记为虚拟的,如果在任何派生类中需要构造函数,它们需要覆盖该虚拟构造函数。

如果基类没有构造函数,您需要添加一个空构造函数。

  TPCEItem = class(TObject)
  public
    constructor Create; virtual;
  end;

  TPCEItemClass = class of TPCEItem;

  TPCEProc = class(TPCEItem)
  public
    constructor Create; override;
    destructor Destroy; override;
  end;


constructor TPCEItem.Create;
begin
  // 如果派生类是TObject
  // 或者是具有空构造函数的任何其他类
  // 您可以省略inherited调用
  inherited;
end;
英文:

When you are constructing objects through meta-class you need to mark its base class constructor as virtual, and if you need a constructor in any of the descendant classes they need to override that virtual constructor.

If the base class does not have a constructor, you will need to add empty one.

  TPCEItem = class(TObject)
  public
    constructor Create; virtual;
  end;

  TPCEItemClass = class of TPCEItem;

  TPCEProc = class(TPCEItem)
  public
    constructor Create; override;
    destructor Destroy; override;
  end;


constructor TPCEItem.Create;
begin
  // if the descendant class is TObject
  // or any other class that has empty constructor
  // you can omit inherited call
  inherited;
end;

答案2

得分: 1

你已经确定了问题 - 基类 TPCEItem 没有定义一个virtual构造函数,它只是继承了 TObject 的构造函数,而这个构造函数不是virtual的。

因此,你不能使用 TPCEItemClass 元类类型来创建任何 TPCEItem 派生类的实例。为了让元类调用正确的派生类构造函数,所引用的基类必须有一个virtual构造函数,例如:

TPCEItem = class(TObject)
...
public
  constructor Create; virtual;
end;
TPCEProc = class(TPCEItem)
...
public
  constructor Create; override;
  ...
end;
procedure TPCEData.CopyPCEItems(...; ItemClass: TPCEItemClass);
var
  AItem: TPCEItem;
  ...
begin
  ...
  AItem := ItemClass.Create; // <-- 现在可以工作了!
  ...	
  if (Obj is TPCEProc) then begin // <-- 注意:使用 'is' 而不是 ClassType 来处理 TPCEProc 的派生类...
    TPCEProc(Obj).CopyProc(TPCEProc(AItem));
  ...
end;
英文:

You have already identified the problem - the base class TPCEItem does not define a virtual constructor, it just inherits a constructor from TObject, which is not virtual.

As such, you cannot create instances of any TPCEItem-derived classes by using your TPCEItemClass metaclass type. In order for a metaclass to invoke the correct derived class constructor, the base class being referred to MUST have a virtual constructor, eg:

  TPCEItem = class(TObject)
  ...
  public
    constructor Create; virtual;
  end;
  TPCEProc = class(TPCEItem)
  ...
  public
	constructor Create; override;
    ...
  end;
procedure TPCEData.CopyPCEItems(...; ItemClass: TPCEItemClass);
var
  AItem: TPCEItem;
  ...
begin
  ...
  AItem := ItemClass.Create; // <-- THIS WORKS NOW!
  ...	
  if (Obj is TPCEProc) then begin // <-- FYI: use 'is' rather than ClassType to handle descendants of TPCEProc...
    TPCEProc(Obj).CopyProc(TPCEProc(AItem));
  ...
end;

答案3

得分: -3

恭喜你已经找出了问题所在的那一行

AItem := ItemClass.Create;  //<--- 有问题的那一行

但是,这一行有什么问题呢?你正在从现有的类实例中调用构造方法。你永远不应该这样做。你应该只从特定的类类型中调用构造方法,而不是现有的类实例。

所以,为了修复你的代码,请将上述的那行改成

AItem := TPCEItem.Create;

也许你可能在考虑调用 AItem := TPCEItemClass.Create;,因为在你的代码中你做了下面的声明

TPCEItemClass = class of TPCEItem; 

这个声明并不意味着 TPCEItemClassTPCEItem 是相同的类型,而是这两个类型具有相同的类型结构,但实际上它们是两个不同的类型。


顺便问一下,你的 CopyPCEItems 过程的参数 ItemClass: TPCEItemClass 是什么目的?如果你在过程中根本没有使用它,而是一直在使用局部变量 AItem: TPCEItem,那么它的存在有什么意义呢?至少在你展示的代码中是这样。

英文:

Congratulations you have identified the problematic line

AItem := ItemClass.Create;  //<--- THE PROBLEMATIC LINE

But what is wrong with this line? You are calling constructor method from existing class instance. You should not do this ever. You should only call constructor methods from specific class types not existing class instances.

So in order to fix your code change the mentioned line to

AItem := TPCEItem.Create;

You may be thinking of perhaps calling AItem := TPCEItemClass.Create; since above in your code you made next declaration

TPCEItemClass = class of TPCEItem; 

This declaration does not meant that TPCEItemClass is the same type as TPCEItem but instead that both types have same type structure but they are in fact two distinct types.


By the way what is the purpose of ItemClass: TPCEItemClass parameter of your CopyPCEItems procedure if you are not even using it in your procedure but instead work with local variable AItem: TPCEItem all the time? Well at least in your shown code that is.

huangapple
  • 本文由 发表于 2023年2月6日 02:50:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/75354707.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定