How can the subclass constructor be called from the parent class?
Asked Answered
S

2

6

Is there a way to invoke Create of the subclass from the parent class? Below there is this Duplicate method in which I want the constructor of the subclass to be invoked instead, so that the test at the bottom succeeds.

type

  IBla<T> = interface(IInvokable)
    ['{34E812BF-D021-422A-A051-A492F25534C4}']
    function GetIntFromIface(): Integer;
    function Duplicate(): IBla<T>;
  end;

  TClassA<T> = class(TInterfacedObject, IBla<T>)
  protected
    function GetInt(): Integer; virtual;
  public
    function GetIntFromIface(): Integer;
    function Duplicate(): IBla<T>;
  end;

  TClassB = class(TClassA<Integer>, IBla<Integer>)
  protected
    function GetInt(): Integer; override;
  end;

function TClassA<T>.Duplicate: IBla<T>;
begin
  Exit(TClassA<T>.Create());
end;

function TClassA<T>.GetInt: Integer;
begin
  Exit(1);
end;

function TClassA<T>.GetIntFromIface: Integer;
begin
  Exit(GetInt());
end;

function TClassB.GetInt: Integer;
begin
  Exit(2);
end;

procedure TestRandomStuff.Test123;
var
  o1, o2: IBla<Integer>;
begin
  o1 := TClassB.Create();
  o2 := o1.Duplicate();    
  Assert.AreEqual(o2.GetIntFromIface, 2);
end;
Stravinsky answered 5/12, 2016 at 8:30 Comment(10)
Any particular reason you want this with generics? Whatever you're trying to achieve looks a little convoluted to me. Are you sure you're simplifying the code to the problem you're trying to solve?Griceldagrid
I am trying to simplify existing code and in doing so have to work within the confines of what is already in place, time constraints, and my Delphi skills.Stravinsky
I'm not sure how well this will play with generics and I'm unable to test it: Declare a virtual constructor for TClassA<T>. Then in TClassA<T>.Duplicate call Result := TClassA<T>(Self.ClassType).Create();Griceldagrid
Apologies, you don't need virtual constructor. You need type TClassAType = class of TClassA<T>; And create the object using: Result := TClassAType(Self.ClassType).Create;Griceldagrid
I tried class references but to the best of my findings they don't work (ie don't compile) with generic types.Stravinsky
@CraigYoung I think that ClassType.Create.GetInterface(IBla<T>, Result) is the magic incantation hereSolorzano
@DavidHeffernan & hell yea, As I said, I'm not in a position to test the generics version. I know the technique works for non-generics (which is why I asked about that). David, I'll take your word for it; I know you're thorough.Griceldagrid
@CraigYoung You cannot declare a metaclass of a generic class. No such thing as a generic metaclass. A bit lame really. So ClassType.Create is I think as close as you can get to your idea.Solorzano
@DavidHeffernan Not even something like TClassAClass<T> = class of TClassA<T>? If so that's really unfortunate.Griceldagrid
@CraigYoung Nope, no generic metaclasses at all. Not even possible as type declarations within the generic class itself.Solorzano
S
4

You can do this using RTTI:

uses
  System.Rtti;

....

function TClassA<T>.Duplicate: IBla<T>;
var
  ctx: TRttiContext;
  typ: TRttiType;
  mthd: TRttiMethod;
  inst: TValue;
begin
  typ := ctx.GetType(ClassInfo);
  mthd := typ.GetMethod('Create');
  inst := mthd.Invoke((typ as TRttiInstanceType).MetaclassType, []);
  inst.AsObject.GetInterface(IBla<T>, Result);
end;

There is quite probably a cleaner way to invoke a constructor using RTTI (I know next to nothing about RTTI in Delphi), so you might do well to read around that topic rather than taking the above as being the canonical way to do this.

Of course, this assumes that all subclasses use a parameterless constructor defined in TObject. That might be rather limiting. I would not be surprised if you found yourself having to re-think the design in a more fundamental manner.

If none of your subclasses implement constructors then you could make it even simpler, and not use RTTI at all:

function TClassA<T>.Duplicate: IBla<T>;
begin
  ClassType.Create.GetInterface(IBla<T>, Result);
end;

But be aware that this calls the constructor defined in TObject and will not call any constructor defined in a subclass.

Solorzano answered 5/12, 2016 at 9:0 Comment(1)
Paremeterless constructors would be just fine, in this case. Thanks, I will try this.Stravinsky
M
0

This seems to work:

function TClassA<T>.Duplicate: IBla<T>;
begin
  //Exit(TClassA<T>.Create());
  Exit( ClassType.Create as TClassA<T> );
end;

The subtlety is that ClassType.Create will create (in this case) a TClassB and the original creates a TClassA< integer > which the compiler sees as different to TClassB, and hence calls TClassA< T >.GetInt rather than TClassB.GetInt.

Edit

But be aware that this calls the constructor defined in TObject and will not call any constructor defined in a subclass. (With thanks to David H)

However, here is solution that overcomes that restriction too:

interface
type

  IBla<T> = interface(IInvokable)
    ['{34E812BF-D021-422A-A051-A492F25534C4}']
    function GetIntFromIface(): Integer;
    function Duplicate(): IBla<T>;
  end;

  TClassA<T> = class(TInterfacedObject, IBla<T>)
  protected
    function GetInt(): Integer; virtual;
  public
    constructor Create; virtual;
    function GetIntFromIface(): Integer;
    function Duplicate(): IBla<T>;
  end;

  //TClassB = class(TClassA<Integer>)
  TClassB = class(TClassA<Integer>, IBla<Integer>)
  protected
    function GetInt(): Integer; override;
  public
    constructor Create; override;
    function Duplicate(): IBla<Integer>;
  end;

procedure Test123;

implementation

constructor TClassA<T>.Create;
begin
  inherited Create;
end;

function TClassA<T>.Duplicate: IBla<T>;
begin
  Exit(TClassA<T>.Create());
end;

function TClassA<T>.GetInt: Integer;
begin
  Exit(1);
end;

function TClassA<T>.GetIntFromIface: Integer;
begin
  Exit(GetInt());
end;

constructor TClassB.Create;
begin
  inherited Create;
end;

function TClassB.Duplicate: IBla<Integer>;
begin
  Result := TClassB.Create;
end;

function TClassB.GetInt: Integer;
begin
  Exit(2);
end;

procedure Test123;
var
  o1, o2: IBla<Integer>;
begin
  o1 := TClassB.Create();
  o2 := o1.Duplicate();
  Assert( o2.GetIntFromIface = 2);
end;
Muck answered 5/12, 2016 at 13:3 Comment(11)
This fails if any subclass declares a constructor. In the sense that the subclass constructor won't be executed.Solorzano
Indeed. I will modify answer accordingly.Muck
You pasted text from my answer verbatim. And using a virtual method seems to defeat the purpose.Solorzano
Sorry you are offended at my plagiarism. As far as virtual methods defeating the object of the exercise - perhaps, but neither of us seem to have a better solution.Muck
David is correct that the core point of this question is to not override the method.Stravinsky
@hellyea Actually, virtual and override can be removed and it will still work. But I guess you mean you do not want to add a Duplicate method to the descendant class at all?Muck
Yes exactly. I want to not have to override for the purpose of gaining access to the class constructor. In my scenario, parameterless constructors are fine.Stravinsky
The problem with that as I see it is that TClassA< T > does not know about the constructor for TClassB. You can do some fairly ugly tests, but short of that the only thing that knows about TClassB is TClassB. Therefore, to do what you want you have to do things like if self is TClassB then somewhere or override Duplicate either implicitly (through interfaces) or explicitly (through inheritance). It would be different if the constructor for TInterfaced object were virtual. However 'AfterConstruction' is virtual, so if you put your constructor details there instead you could do it, I think.Muck
You perceive this as a problem, fine. Still Delphi lets me do it with classes that are not parameterized, and I don't mind being a bit hackish to make this work. docwiki.embarcadero.com/RADStudio/Seattle/en/…Stravinsky
I don't think that Delphi does let you do it with unparameterised classes. I think that the problems are exactly the same. The solution using AfterConstruction does work though. If this is suitable for you, I will publish it.Muck
@Muck You can use RTTI as per my answerSolorzano

© 2022 - 2024 — McMap. All rights reserved.