4
votes

I am trying to figure out how to genericize this helper method; so that it returns the same type that was asked for:

type
    TScreenHelper = class helper for TScreen
    public
        function FindForm(DesiredFormClass: TFormClass): TForm;
    end;

Right now the caller has to cast the return value to type they already want:

var
   frmReportReminderSetup: TfrmReportReminderSetup;
begin
   //frmReportReminderSetup := Screen.FindForm(TfrmReportReminderSetup); Doesn't compile

   frmReportReminderSetup := TfrmReportReminderSetup(Screen.FindForm(TfrmReportReminderSetup));

The non-generic implementation is:

function TScreenHelper.FindForm(DesiredFormClass: TFormClass): TForm;
var
    f: TForm;
    i: Integer;
begin
    Result := nil;

    for i := 0 to Screen.FormCount-1 do //Screen.Forms does not support enumeration
    begin
        f := Screen.Forms[i];

        if (f is DesiredFormClass) then
        begin
            Result := f;
            Exit;
        end;
    end;
end;

Generics

What i want is some way to use generics so that the function returns the type it was asked for.

frmContoso := Screen.FindForm(TfrmContoso);

In pseudo-code, the signature that i want would be something like:

function FindForm(T: TFormClass): T;

When it comes time to turn this into actual Delphi syntax, i think you must specify one of the T references in angle brackets:

function FindForm(<T>): T;

But i don't think the is allowed there; i think it has to be before the opening parenthesis:

function FindForm<T>: T;

Try that

TScreenHelper = class helper for TScreen
public
    function FindFormOld(DesiredFormClass: TFormClass): TForm;
    function FindForm<T>: T;
end;

function TScreenHelper.FindForm<T>: T;
var
    f: TForm;
    i: Integer;
begin
    Result := nil;

    for i := 0 to Screen.FormCount-1 do //Screen.Forms does not support enumeration
    begin
        f := Screen.Forms[i];

        if (f is T) then
        begin
            Result := f as T;
            Exit;
        end;
    end;
end;

Except that fails to compile:

Result := nil;  E2010 Incompatible types: 'T' and 'Pointer'

I can sorta see what's wrong. It doesn't understand that T is a class (i.e. what if it was an Integer? Then it would absolutely be wrong to set it to nil.)

Constraints

So i need to somehow give the compiler the hint of what type T will be:

TScreenHelper = class helper for TScreen
public
    function FindForm<T: TFormClass>: T;
end;

function TScreenHelper.FindForm<T: TFormClass>: T;
var
    f: TForm;
    i: Integer;
begin
    Result := nil;

    for i := 0 to Screen.FormCount-1 do //Screen.Forms does not support enumeration
    begin
        f := Screen.Forms[i];

        if (f is T) then
        begin
            Result := f as T;
            Exit;
        end;
    end;
end;

This new signature is confusing to call; you no longer pass a desired type to the function. Instead you now call a variation of the function that you want:

frmContoso := Screen.FindForm<TfrmConsoto>();

But no matter; it's way of generics.

Except that's not valid

The syntax:

function FindForm<T: TFormClass>: T;

is not valid, as TFormClass is not one of the allowed Delphi constraint types:

Constraints in Generics

Specifying Generics with Constraints

Constraint items include:

  • Zero, one, or multiple interface types
  • Zero or one class type
  • The reserved word "constructor", "class", or "record"

(emphasis mine)

While i'm allowed one class type, i'm not passing a class type; i'm passing a class of class type.

So now i'm stuck. In my attempt to save myself typing 25 characters, i've now blown an hour on the minutia of Delphi generics.

tl;dr

How do i genericize:

function FindForm(DesiredFormClass: TFormClass): TForm;

Example non-working code

program Project2;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, Vcl.Forms;

type
    TScreenHelperCore = class(TObject)
    public
        class function FindForm<T: TForm>: T;
    end;

    TfrmContoso = class(TForm)
    public

   end;

{ TScreenHelperCore }

class function TScreenHelperCore.FindForm<T: TForm>: T; 
//                                         \__[dcc32 Error] Project2.dpr(23): E2029 ',', ';' or '>' expected but ':' found
var
    f: TForm;
    i: Integer;
begin
    Result := nil;

    for i := 0 to Screen.FormCount-1 do //Screen.Forms does not support enumeration
    begin
        f := Screen.Forms[i];

        if (f is T) then
        begin
            Result := f;
            Exit;
        end;
    end;
end;

var
    f: TfrmContoso;

begin
  try
        f := TScreenHelperCore.FindForm<TfrmContoso>;
        if f = nil then
            f := TfrmContoso.Create(nil);

        f.ShowModal;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
1
Ian @XE6!? What happened to D5? ;)NGLN
How come I know who wrote this question after only reading 3-4 pages :)LU RD
@NGLN Keep up with the times. We've moved on!!David Heffernan
@LURD Indeed. I got it from the first page on my phone!David Heffernan
@NGLN My employer decided to fork over the $3,000 for a new version. And three months later the bugridden upgrade is already out of date :(Ian Boyd

1 Answers

5
votes

Your constraint is wrong. Instead of

function FindForm<T: TFormClass>: T;

you need

function FindForm<T: TForm>: T;

You are going to instantiate this generic type with TMyForm rather than class of TMyForm.

And you must only state the constraint in the declaration of the class and not in its implementation. Here's a complete program that compiles:

{$APPTYPE CONSOLE}
uses
  Vcl.Forms;

type
  TScreenHelper = class helper for TScreen
  public
    function FindForm<T: TForm>: T;
  end;

function TScreenHelper.FindForm<T>: T;
var
  f: TForm;
  i: Integer;
begin
  for i := 0 to Screen.FormCount - 1 do
  begin
    f := Screen.Forms[i];
    if (f is T) then
    begin
      Result := T(f);
      Exit;
    end;
  end;
  Result := nil;
end;

type
  TMyForm = class(TForm)
  end;

var
  Form: TMyForm;

begin
  Form := Screen.FindForm<TMyForm>;
end.