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;
  • 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? – Disillusioned Dec 05 '16 at 08:57
  • 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. –  Dec 05 '16 at 09:06
  • I'm not sure how well this will play with generics and I'm unable to test it: Declare a virtual constructor for `TClassA`. Then in `TClassA.Duplicate` call `Result := TClassA(Self.ClassType).Create();` – Disillusioned Dec 05 '16 at 09:14
  • Apologies, you don't need virtual constructor. You need `type TClassAType = class of TClassA;` And create the object using: `Result := TClassAType(Self.ClassType).Create;` – Disillusioned Dec 05 '16 at 09:49
  • I tried class references but to the best of my findings they don't work (ie don't compile) with generic types. –  Dec 05 '16 at 10:17
  • @CraigYoung I think that `ClassType.Create.GetInterface(IBla, Result)` is the magic incantation here – David Heffernan Dec 05 '16 at 11:54
  • @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. – Disillusioned Dec 05 '16 at 12:22
  • @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. – David Heffernan Dec 05 '16 at 12:25
  • @DavidHeffernan Not even something like `TClassAClass = class of TClassA`? If so that's really _unfortunate_. – Disillusioned Dec 05 '16 at 12:27
  • @CraigYoung Nope, no generic metaclasses at all. Not even possible as type declarations within the generic class itself. – David Heffernan Dec 05 '16 at 12:34

2 Answers2

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Paremeterless constructors would be just fine, in this case. Thanks, I will try this. –  Dec 05 '16 at 09:03
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;
Dsm
  • 5,870
  • 20
  • 24
  • This fails if any subclass declares a constructor. In the sense that the subclass constructor won't be executed. – David Heffernan Dec 05 '16 at 13:43
  • Indeed. I will modify answer accordingly. – Dsm Dec 05 '16 at 13:53
  • You pasted text from my answer verbatim. And using a virtual method seems to defeat the purpose. – David Heffernan Dec 05 '16 at 14:05
  • 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. – Dsm Dec 05 '16 at 14:12
  • David is correct that the core point of this question is to not override the method. –  Dec 05 '16 at 14:20
  • @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? – Dsm Dec 05 '16 at 14:29
  • 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. –  Dec 05 '16 at 14:42
  • 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. – Dsm Dec 05 '16 at 14:56
  • 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. http://docwiki.embarcadero.com/RADStudio/Seattle/en/Class_References#Constructors_and_Class_References –  Dec 05 '16 at 15:01
  • 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. – Dsm Dec 05 '16 at 16:04
  • @Dsm You can use RTTI as per my answer – David Heffernan Dec 05 '16 at 18:00