Declaring Generics

From RAD Studio
Jump to: navigation, search

Go Up to Generics Index

The declaration of a generic is similar to the declaration of a regular class, record, or interface type. The difference is that a list of one or more type parameters placed between angle brackets (< and >) follows the type identifier in the declaration of a generic.

A type parameter can be used as a typical type identifier inside a container type declaration and method body.

For example:

 type
   TPair<TKey,TValue> = class   // TKey and TValue are type parameters
     FKey: TKey;
     FValue: TValue;
     function GetValue: TValue;
   end;
 
 function TPair<TKey,TValue>.GetValue: TValue;
 begin
   Result := FValue;
 end;
Note: You must call the default constructor and initialize the class fields before calling the GetValue method.

Type Argument

Generic types are instantiated by providing type arguments. In Delphi, you can use any type as a type argument except for the following: a static array, a short string, or a record type that (recursively) contains a field of one or more of these two types.

 type
   TFoo<T> = class
     FData: T;
   end;
 var
   F: TFoo<Integer>; // 'Integer' is the type argument of TFoo<T>
 begin
   ...
 end.

Nested Types

A nested type within a generic is itself a generic.

type
  TFoo<T> = class                   // A generic class
  type
    TBar = class
      X: Integer;
    end;
  end;

To access the TBar nested type, you must specify a construction of the TFoo type first:

 var
   N: TFoo<Double>.TBar;

A generic can also be declared within a regular class as a nested type:

Type
  TBaz = class                     // A regular class
  type
    TQux<T> = class
      X: Integer;
    end;
  end; 

type
   TOuter = class
   type
     TData<T> = class
       FFoo1: TFoo<Integer>;         // declared with closed constructed type
       FFoo2: TFoo<T>;               // declared with open constructed type
       FFooBar1: TFoo<Integer>.TBar; // declared with closed constructed type
       FFooBar2: TFoo<T>.TBar;       // declared with open constructed type
       FBazQux1: TBaz.TQux<Integer>; // declared with closed constructed type
       FBazQux2: TBaz.TQux<T>;       // declared with open constructed type
       ...
     end;
   var
     FIntegerData: TData<Integer>;
     FStringData: TData<String>;
   end;

Base Types

The base type of a parameterized class or interface type might be an actual type or a constructed type. The base type might not be a type parameter alone.

  type
    TFoo1<T> = class(TBar)            // Actual type
    end;
 
    TFoo2<T> =  class(TBar2<T>)       // Open constructed type
    end;

    TFoo3<T> = class(TBar3<Integer>)  // Closed constructed type
    end;

If TFoo2<String> is instantiated, an ancestor class becomes TBar2<String>, and TBar2<String> is automatically instantiated.

Class, Interface, and Record Types

Class, interface, record, and array types can be declared with type parameters.

For example:

 type
   TRecord<T> = record
     FData: T;
   end;
 
 type
   IAncestor<T> = interface
     function GetRecord: TRecord<T>;
   end;
 
   IFoo<T> = interface(IAncestor<T>)
     procedure AMethod(Param: T);
   end;
 
 type
   TFoo<T> = class(TObject, IFoo<T>)
     FField: TRecord<T>;
     procedure AMethod(Param: T);
     function GetRecord: TRecord<T>;
   end;

 type
   anArray<T>= array of T;
   IntArray= anArray<integer>;

Procedural Types

The procedure type and the method pointer can be declared with type parameters. Parameter types and result types can also use type parameters.

For example:

 type
   TMyProc<T> = procedure(Param: T);
   TMyProc2<Y> = procedure(Param1, Param2: Y) of object;
 type
   TFoo = class
     procedure Test;
     procedure MyProc(X, Y: Integer);
   end;
 
 procedure Sample(Param: Integer);
 begin
   Writeln(Param);
 end;
 
 procedure TFoo.MyProc(X, Y: Integer);
 begin
   Writeln('X:', X, ', Y:', Y);
 end;
 
 procedure TFoo.Test;
 var
   X: TMyProc<Integer>;
   Y: TMyProc2<Integer>;
 begin
   X := Sample;
   X(10);
   Y := MyProc;
   Y(20, 30);
 end;
 
 var
   F: TFoo;
 begin
   F := TFoo.Create;
   F.Test;
   F.Free;
 end.

Parameterized Methods

Methods can be declared with type parameters. Parameter types and result types can use type parameters. However, constructors and destructors cannot have type parameters, and neither can virtual, dynamic, or message methods. Parameterized methods are similar to overloaded methods.

There are two ways to instantiate a method:

  • Explicitly specifying type argument
  • Automatically inferring from the type argument

For example:

type
  TFoo = class
    procedure Test;
    procedure CompareAndPrintResult<T>(X, Y: T);
  end;

procedure TFoo.CompareAndPrintResult<T>(X, Y: T);
var
  Comparer : IComparer<T>;
begin
  Comparer := TComparer<T>.Default;
  if Comparer.Compare(X, Y) = 0 then
    WriteLn('Both members compare as equal')
  else
    WriteLn('Members do not compare as equal');
end;

procedure TFoo.Test;
begin
  CompareAndPrintResult<String>('Hello', 'World');
  CompareAndPrintResult('Hello', 'Hello');
  CompareAndPrintResult<Integer>(20, 20);
  CompareAndPrintResult(10, 20);
end;

var
  F: TFoo;
begin
  F := TFoo.Create;
  F.Test;
  ReadLn;
  F.Free;
end.

Scope of Type Parameters

The scope of a type parameter covers the type declaration and the bodies of all its members, but does not include descendent types.

For example:

 type
   TFoo<T> = class
     X: T;
   end;
 
   TBar<S> = class(TFoo<S>)
     Y: T;  // error!  unknown identifier "T"
   end;
 
 var
   F: TFoo<Integer>;
 begin
   F.T  // error! unknown identifier "T"
 end.

See Also