Jump to content
Ali Dehban

Compile time issue with RTTI, generic interface and type casting...

Recommended Posts

Hi mates,
I have something on my mind but I couldn't implement it correctly, imagine a generic interface, some classes inherited from that interface, and one method in each class with the same name, now I'm trying to use this interface type every where for different approaches but it doesn't compile correctly.
Please have a look at the code if you get a chance and share your thoughts with me, I do appreciate you in advance.
The question is how can I implement such an idea properly and safely?
I have attached a sample project to save you time too.

 

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Rtti;

type
  IMyInterface<T> = interface
      function DoSomething: T;
    end;

  TMyClass<T> = class(TInterfacedObject, IMyInterface<T>)
    function DoSomething: T;
  end;

  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    function UseInterface<T>(obj: IMyInterface<T>): T;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TMyClass<T> }

function TMyClass<T>.DoSomething: T;
var
  ctx: TRttiContext;
  typ: TRttiType;
begin
  ctx := TRttiContext.Create;
  typ := ctx.GetType(TypeInfo(T));

  if typ.TypeKind = tkInteger then
    Result := 20 // E2010 Incompatible types: 'T' and 'Integer'
  else if typ.TypeKind = tkString then
    Result := T('Hello')  //   E2089 Invalid typecast
  else
  if typ.AsInstance.MetaclassType.InheritsFrom(TStringList) then
    Result := (typ.AsInstance.MetaclassType.InitInstance(typ) as TStringList) //E2010 Incompatible types: 'T' and 'TStringList'
  else
    Result := Default(T);

  ctx.Free;
end;

{ TForm1 }

function TForm1.UseInterface<T>(obj: IMyInterface<T>): T;
begin
  Result := obj.DoSomething;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  obj1: IMyInterface<Integer>;
  obj2: IMyInterface<String>;
  obj3: IMyInterface<TStringList>;
begin
  try
    obj1 := TMyClass<Integer>.Create;
    obj2 := TMyClass<String>.Create;
    obj3 := TMyClass<TStringList>.Create;

    ShowMessage(UseInterface<Integer>(obj1).ToString);
    ShowMessage(UseInterface<String>(obj2));
    ShowMessage(UseInterface<TStringList>(obj3).Text);
  except
    on E: Exception do
      Writeln('Exception: ', E.ClassName, ': ', E.Message);
  end;
end;

end.

 

Generic Interface.zip

  • Like 1

Share this post


Link to post

Generics in Delphi don't work the way you seem to think they do. To use the generic type T inside a method of the generic class  with a specific type the compiler needs some minimal information on what T can be. That is what generic constraints are for. Unfortunately the constraints supported at the moment are quite limited and will not cover what you intend to do.

 

 

  • Like 2

Share this post


Link to post

First, thank you for your response.
Second, I thought so but!

It seems using RTTI's TValue it is possible to handle this situation somehow.
I'm just not sure if everything goes right with this code or not at the end, I tested with "ReportMemoryLeaksOnShutdown := True;" and there is no memory leak.
Please have a look at the final version and share your thoughts with me.

Best regards.

 

type
  IMyInterface<T> = interface
  ['{D68085A3-6AFB-46D9-A4EE-B46563758127}']
      function DoSomething: T;
    end;

  TMyClass<T> = class(TInterfacedObject, IMyInterface<T>)
    function DoSomething: T;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    function UseInterface<T>(obj: IMyInterface<T>): T;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TMyClass<T> }

function TMyClass<T>.DoSomething: T;
var
  ctx: TRttiContext;
  typ: TRttiType;
  val: TValue;
begin
  ctx := TRttiContext.Create;
  typ := ctx.GetType(TypeInfo(T));

  if typ.TypeKind = tkInteger then
    val := 20
  else if (typ.TypeKind = tkString) or (typ.TypeKind = tkUString) then
    val := 'Hello'
  else
  if typ.AsInstance.MetaclassType.InheritsFrom(TStringList) then
  begin
    val := TStringList.Create;
    val.AsType<TStringList>.Add('Hello from StringList');
  end;

  Result := Val.AsType<T>;
  ctx.Free;
end;

{ TForm1 }

function TForm1.UseInterface<T>(obj: IMyInterface<T>): T;
begin
  Result := obj.DoSomething;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  obj1: IMyInterface<Integer>;
  obj2: IMyInterface<String>;
  obj3: IMyInterface<TStringList>;
  lvstr: TStringList;
begin
  try
    try
      obj1 := TMyClass<Integer>.Create;
      obj2 := TMyClass<String>.Create;
      obj3 := TMyClass<TStringList>.Create;

      ShowMessage(UseInterface<Integer>(obj1).ToString);
      ShowMessage(UseInterface<String>(obj2));

      lvstr := UseInterface<TStringList>(obj3);
      ShowMessage(lvstr.Text);
    except
      on E: Exception do
        ShowMessage('Exception: ' + E.ClassName + ': ' + E.Message);
    end;
  finally
    lvstr.Free;

    obj1 := nil;
    obj2 := nil;
    obj3 := nil;
  end;
end;

 

Generic Interface-final.zip

  • Like 1

Share this post


Link to post

Well, it may work but I fail to see the usefulness of such a construct.

Share this post


Link to post

I see.
Well, beyond the curiosity, it depends on the nature of the project.
As a general sample, it could/might be useful to implement something like an ORM, especially in an Active-Record structure or like that. (More info about the Active-Record: https://guides.rubyonrails.org/active_record_basics.html)
In my case, I need it in a multi-purpose data access layer of a project but with complex data types, not Integer and String parameters combined heavily to RTTI functionalities.

Thank you for your feedback.

Share this post


Link to post
On 2/27/2024 at 11:00 PM, Ali Dehban said:

It seems using RTTI's TValue it is possible to handle this situation somehow.

You can do it without TValue

 

if TypeInfo(T) = TypeInfo(Integer) then PInteger(@Result)^ := 10

else if TypeInfo(T) = TypeInfo(string) then PString(@Result)^ := 'Hello'

  • Like 2

Share this post


Link to post
Posted (edited)

@Ali Dehban I like the idea of the area you were investigating here.  
Stretching  my brain to a function that can return any type is something I have thought about in the last year or so.

 

It took a while before I started to wrap my head around enough it to have questions, 'tho.

 

The first was that   

  IMyInterface<T> = interface
      function DoSomething: T;
    end;

looks a lot like the definition of an anonymous function ... and then I replaced it with records.  But I'll skip that for now.

 

My second eventual question is why have

function UseInterface<T> ( obj: IMyInterface<T> ) : T;
begin
  Result := obj.DoSomething;
end;

 vs just ?

obj.DoSomething;

So, as a first step I ended up with the following :  (I realise you would have been simplifying the code above from your real use case)

{$APPTYPE CONSOLE}
program Project3;
uses
    System.SysUtils, System.Variants, System.Classes, System.Rtti;

type
    IWithAny<T> = interface
        function DoSomething: T;
    end;

    TWithAny<T> = class(TInterfacedObject, IWithAny<T>)
        function DoSomething : T;
    end;

{ TWithAny<T> }
function TWithAny<T>.DoSomething: T;
begin
    var val: TValue := TValue.Empty;
    var typ: T      := default(T);

    case GetTypeKind(T) of
        tkString,
        tkUString  :  val := 'Hello';
        tkInteger  :  val := 20;
        tkClass    :  if TValue.From<T>(typ).IsType<TStringList> then
                      begin
                         val := TStringList.Create;
                         val.AsType<TStringList>.Add('Hello from StringList');
                      end;
    end;
    Result := Val.AsType<T>;
end;

var
    obj1 :  IWithAny< Integer >;
    obj2 :  IWithAny< String >;
    obj3 :  IWithAny< TStringList >;
begin
    obj1 := TWithAny< Integer >    .Create;
    obj2 := TWithAny< String >     .Create;
    obj3 := TWithAny< TStringList >.Create;

    writeln( obj1.DoSomething );
    writeln( obj2.DoSomething );

    var lvstr := obj3.DoSomething;
    writeln(lvstr.Text);
    lvstr.Free;

    readln;
    ReportMemoryLeaksOnShutdown := True;
end.

What I'd kinda like to see is the interface return a (managed record?) holding the <T>, made to clean up it's own memory.

Edited by pmcgee
  • Like 1

Share this post


Link to post
On 3/3/2024 at 12:11 AM, Remy Lebeau said:

GetTypeKind(T)

@pmcgee Hi.
In most scenarios your sample would be enough, I agree.

But why I used this:

function UseInterface<T> ( obj: IMyInterface<T> ) : T;
begin
  Result := obj.DoSomething;
end;

Because I wanted to call ClassObj.DoSomething from within another class and using one function which is not necessary but I wanted to know how it works in Delphi.
When you are calling something like these generic object's generic members/methods from within another object(different class) then Delphi forces you to write like that.

 

For instance, something like this won't be compiled which was my desired call but it's not allowed:

function UseInterface(obj: T): T;

Besides, I wanted to limit the input type to classes (no primitive types at all) which is possible if I use such a function, so my final function is even more complicated like this:  

function UseInterface<T: class>(obj: IMyInterface<T>): T;

The keyword "class" here can limit the input type only to classes, so Integer or String is no longer allowed here.

Share this post


Link to post

@Ali Dehban  can you expand on that ? 

 

I'm trying to picture why if you have an obj ( where the type = IMyInterface<xxx> ) anywhere in your code, you couldn't use obj.DoSomething .
Isn't that the point of the interface? 

Share this post


Link to post

You're absolutely right, this is the normal usage, but at the moment you want to have one function to avoid repeating with the "obj" as input parameter then Delphi forces you to write that function in this way.

 

As I mentioned this is not necessary and what you wrote is ok 99 percent of the time.

 

There is one benefit with such a function that is the possibility to limit the input type to classes.(No primitive types like integer)

I'm writing a library and I wanted to force the developers to use it in this way in this case.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×