Using Generics in Delphi

Published 23 December 9 3:55 PM | Phil Gilmore

GenericsLogoBracketsWhite Phil Gilmore

12/23/2009, revised 02/25/2010

Introduction

Generics were introduced in Delphi in version 2009.  They were implemented as first class citizens in the Object Pascal language.  They work splendidly and support all the features you would expect.  This is a huge improvement for this struggling language.  If you are in the majority of Delphi users who refuse to move past Delphi 7, you now have plenty of reason to upgrade. 

 

What’s so great about generics?

Once you’ve written a class using generic type parameters (generics), you can use that class with any type and the type you choose to use with any given use of that class replaces the generic types you used when you created the class.  The chosen type now appears as your method parameters, return types and property types.  You don’t have to cast or convert the types… it’s as if the class was written with that type in mind all along.

For example, most of us have used the TObjectList.  You can use it by itself and just stick objects in it and pull them out again.  But that requires you to cast the objects from TObject back out to TWhateverMyClassNameIs every time you fetch them.  You can also write a descendant class from TObjectList and override the TObject members with your custom type.  But then the class only works with your type.

Imagine using a generic TObjectList.  TObjectList<TMyCustomType> and its methods accept parameters of type TMyCustomType and its collection yields TMyCustomType objects instead of TObject.  And you never have to write code to make it aware of TMyCustomType.  That’s what generics can do for you. 

This is done by building the class with a placeholder for a runtime type.  At runtime, you provide that type to the class when you declare it.  Then that type replaces your placeholder throughout the class when you instantiate an object from it.

First look

Syntactically, you’ll initially be surprised how much Delphi generics resemble C# generics.  There are few differences and they’re similar enough that you won’t have any trouble switching gears between the two.  Let’s look at a generic class.

unit GenericSample1;

interface

type
  TGenericArray<T> = array of T;

type
  TMyData<T> = class(TObject)
  public
    function Add(const aExistingSet: TGenericArray<T>;
      const aNewValue: T): TGenericArray<T>;
  end;

implementation

{ TMyData<T> }

function TMyData<T>.Add(const aExistingSet: TGenericArray<T>;
  const aNewValue: T): TGenericArray<T>;
begin
  { You could append the new 
value to the existing set and return it here. }
end; end.

 

A class can have a type parameter (usually named T when only one parameter is present) as shown above.  You can also have a parameterized method like this:

unit ParameterizedMethodExample1;

interface

type
  TMyData<T> = class(TObject)
  public
    procedure ParameterizedMethod<T2>(const aGenericParameter: T2);
  end;

implementation


{ TMyData<T> }

procedure TMyData<T>.ParameterizedMethod<T2>(const aGenericParameter: T2);
begin
  // Do something with T2.
end;

end.


You can have multiple parameters:

unit MultipleParametersExample1;

interface

type
  TMyData<T, T2> = class(TObject)
  public

  end;

implementation


end.

 

 

Generic Constraints

Given that the type is unknown, it’s sometimes difficult to do much with a member or parameter of a generic type, because an unknown type has unknown members and the compiler won’t let you assume what those members are.  It must know the type. 

You can assert some assumptions using generic type constraints.  This is where we first deviate from C# syntax.  In C#, your constraints are declared at the end of the class or method declaration, prefaced with the where keyword.  Here are some C# constraint declarations.

public class MyData<T> where T: class { }

public class MyData<T> where T: class, IMydata { }

public class MyData<T> where T: class, IMydata, new { }

It almost looks a little Pascal-ish, doesn’t it?  Instead of a suffix of the declaration, Delphi neatly declares constraints inline as if it were a type, in traditional Pascal fashion.  Here are the equivalent Delphi declarations.

unit ConstraintsExample;

interface

type
  IMyInterface = interface ['{78B5F879-BE4A-436A-B4FB-96A5329B3386}']
    function GetText: string;
  end;

type
  TMyConcreteClass = class(TInterfacedObject, IMyInterface)
  public
    function GetText: string;
  end;

type
  TMyData1<T: class> = class(TObject)
  public
    procedure WhatCanItDo(const aParm: T);
  end;
type
  TMyData2<T: TMyConcreteClass, IMyInterface> = class(TObject)
  public
    procedure WhatCanItDo(const aParm: T);
  end;
type
  TMyData3<T: class, IMyInterface, constructor> = class(TObject)
  public
    function WhatCanItDo(const aParm: T): T;
  end;

implementation


{ TMyData1<T> }

procedure TMyData1<T>.WhatCanItDo(const aParm: T);
var
  p: pointer;
  obj: TObject;
begin
  // Here, <T: class> tells us it descends from TObject
  obj := TObject(aParm);

  // We also know that it is a reference type.
  p := Pointer(TObject(aParm));
end;

{ TMyData2<T> }

procedure TMyData2<T>.WhatCanItDo(const aParm: T);
var
  intf: IMyInterface;
  conc: TMyConcreteClass;
begin
  // Here, <T: TMyConcreteClass, IMyInterface>
  // tells us T descends from TMyConcreteClass,
  conc := TMyConcreteClass(aParm);

  // and that T implements IMyInterface.
  intf := TMyConcreteClass(aParm) as IMyInterface;
end;

{ TMyData3<T> }

function TMyData3<T>.WhatCanItDo(const aParm: T): T;
begin
  // Here, <T: class, IMyInterface, constructor> tells us that
  // the T type has a default (parameterless) constructor
  // and we can create an instance of that type.
  // We also know that any instance we create implements
  // the IMyInterface interface.

  result := T.Create;
end;

{ TMyConcreteClass }

function TMyConcreteClass.GetText: string;
begin
  result := 'Sample text.';
end;

end.

 

In the code above, I show constraints using the class, interface and constructor elements.  Constraints may have a base class instead of the class element, indicating that the type must descend from the base class indicated.  You may also use the record element to indicate that the type is a value type or record type. 

Given the constraints in the examples above, TMyData1 must receive a class for its type parameter.  It cannot be a primitive or a record.  TMyData2 and TMyData3 must receive a class that is or descends from TMyConcreteClass.  Any primitive, record or incompatible class type will generate a compiler error.  

Here are example declarations to consume these classes:

var
  data1: TMyData1<TObject>;
  data2: TMyData2<TMyConcreteClass>;
  data3: TMyData3<TMyConcreteClass>;

 

Here are some declarations that violate our constraints.

var mustBeObjectData: TMyData1<TMyRecord>; mustBeConcreteType: TMyData2<TObject>;

 

Here are the errors our violations will raise at compile time.

[DCC Error] Unit1.pas(48): E2511 
Type parameter ‘T’ must be a class type
[DCC Error] Unit1.pas(49): E2515 
Type parameter ‘T’ is not compatible with type ‘TMyConcreateClass’
[DCC Fatal Error] Project1.dpr(6): F2063 
could not compile used unit ‘Unit2.pas’

 

The first is because we pass a record where the constraint demands a class, and the second is because we pass a TObject where the constraint demands a class the descends from TMyConcreteClass, which TObject does not do.

 

Multiple parameter syntaxes

When using multiple type parameters, syntax has some strange complications.  So far, we’ve looked at the syntax for a single type Parameter.

 

public
  function Method1<T>: T;

 

The syntax for multiple parameters is a comma-separated list of type parameters.  You can optionally use a semicolon to separate the methods as well.

public
  function Method2<T1, T2>: T2;
  function Method3<T1; T2>: T2;

 

However, type parameter constraints will throw a wrench into the mix.  If you add a constraint to any of the type parameters, the comma is no longer allowed and the semicolon is required.

public
  function Method4<T1: class; T2>: T2;
  function Method5<T1; T2: IInterface>: T2;
  function Method6<T1: class; T2: IInterface>: T2;

 

There is no difference in the implementation section.  A comma-separated list of type parameter names is all that is needed.  However, the the parameter names must be complete and correct, and there is currently a bug in Delphi 2010 where code completion does not generate the implementation declarations properly (only the first parameter is included in the method implementation).  Here is a proper set of declarations for all the examples above:

function TMyClass.Method1<T>: T; begin end;
function TMyClass.Method2<T1, T2>: T2; begin end;
function TMyClass.Method3<T1, T2>: T2; begin end;
function TMyClass.Method4<T1, T2>: T2; begin end;
function TMyClass.Method5<T1, T2>: T2; begin end;
function TMyClass.Method6<T1, T2>: T2; begin end;

 

Scope

A type parameter in a class type parameter list is visible anywhere inside that class.  It can be used for method parameters, method return values, field types, property types or method local variables.

A method can also have a generic type parameter list and those parameter lists also support constraints.  A type parameter in a method type parameter list is visible anywhere inside that method. 

 

Here is a unit which demonstrates various generic parameter scopes.

unit ScopeExample;

interface
uses Classes;

type
  TScopeExample<T: class> = class(TObject)
  public
    fGenericField: T;
    function GenericMethod<T2: TStringList>(const aParameter1: T; const aParameter2: T2): T2;
    procedure UsedAsParameterType(const aParameter: T);
    function UsedAsMethodReturnType: T;
    property UsedAsPropertyType: T
      read UsedAsMethodReturnType
      write UsedAsParameterType;
  end;

implementation

{ TScopeExample<T> }

function TScopeExample<T>.GenericMethod<T2>(const aParameter1: T;
  const aParameter2: T2): T2;
begin
  if (T.ClassInfo = TStringList.ClassInfo) then
    result := T2(aParameter1)
  else
    result := aParameter2;
end;

function TScopeExample<T>.UsedAsMethodReturnType: T;
var
  lUsedAsLocalVariable: T;
begin
  result := lUsedAsLocalVariable;
end;

procedure TScopeExample<T>.UsedAsParameterType(const aParameter: T);
var
  lUsedAsLocalVariable: T;
begin
  lUsedAsLocalVariable := aParameter;
end;

end.

 

Generics in VCL

There is a concept of self-obsolescence behind generics.  Because you now have generics, you may quickly go through your previous patterns and adopt them to use generics.  But once they’re done, you find that they’re so reusable that you need not write any code to accommodate your most common patterns anymore after that.  Well, there’s more news.  Be it good or bad, The VCL has implemented many of those patterns using generics.  So you may not need to make those conversions in the first place.  Let’s explorer some of the generic structures and classes that the VCL now provides.

While generic declarations are now lightly sprinkled throughout the VCL, the starting point for most generics support is implemented in three basic units. 

uses
SysUtils,
Generics.Defaults,
Generics.Collections;

In general, support for iterators and comparers  are declared in Generics.Defaults, and collection structures are declared in Generics.Collections.

Here are the public declarations in Generics.Defaults.pas:

 

Classes / Interfaces Description
IComparer<T> Implements a compare function returning less-than, greater-than or equal-to.
IEqualityComparer<T> Implements a compare function returning equal-to or not.
TComparer<T> Abstract class for implementing IComparer<T>.
TEqualityComparer<T> Abstract class for implementing IEqualityComparer<T>
TSingletonImplementation Weak-reference implementation of TInterfacedObject.  Instances are not automatically freed.
TDelegatedEqualityComparer<T> A comparer that takes a delegated comparison function and uses it to determine equality.
TDelegatedComparer<T> A comparer that takes a delegated function to do the actual comparison.
TCustomComparer<T> An abstract singleton class for comparison operations.
TStringComparer Compares strings.
Method prototypes / delegates  
TComparison<T> A delegate method prototype for less-than, greater-than, or equal-to comparisons.
TEqualityComparison<T> A delegate method prototype for equality comparisons.
THasher<T> A delegate method prototype for getting the hash value of an object or value.
Concrete Functions  
function BobJenkinsHash(const Data;
Len, InitData: Integer): Integer;
Returns a hash from the given data.
function BinaryCompare(const Left, Right: Pointer;
Size: Integer): Integer;
A method to compare binary data.  Does not match comparison prototypes, so must be used from a comparison class rather than pass as a delegated method.

 

Here are the public declarations in Generics.Collections.pas:

Classes / Interfaces Description
TArray Contains no data, but provides methods for working with arrays.  Strangely, the class is not generic and the members are.  This can easily be confused with TArray<T>, which is in System.pas, which contains data and no methods.
TEnumerator<T> Implements the iterator pattern on your data.
TEnumerable<T> Returns an enumerator, allowing your class to be used in a For..In construct.
TList<T> A generic collection or objects, records or primitives.
TQueue<T> A generic queue (first in / last out) collection.
TStack<T> A generic stack (first in / first out) collection.
TPair<TKey, TValue> A structure containing two child members of disparate types.
TDictionary<TKey, TValue> A searchable collection with lookup keys.
TObjectList<T: class> A generic collection of objects (records and primitives not allowed).
TObjectQueue<T: class> A generic queue of objects.
TObjectStack<T: class> A generic stack of objects.
TObjectDictionary<TKey, TValue> A generic collection of owned objects with lookup keys.
Enums  
TCollectionNotification Used in TCollectionNotifyEvent<>
TDictionaryOwnerships Used by TObjectDictionary<>
Method prototypes / delegates  
TCollectionNotifyEvent<T> Used by various collections to implement an observer pattern.
Concrete functions  
InCircularRange  
Type aliases  
PObject Pointer to TObject.
Exceptions  
ENotsupportedException  

 

Here are the pertinent declarations in SysUtils.pas:

Method prototypes / delegates Description
TProc A procedure with no parameters.
TProc<T> A procedure with one generic parameter.
TProc<T1, T2> A procedure with two generic parameters.
TProc<T1, T2, T3> A procedure with three generic parameters.
TProc<T1, T2, T3, T4> A procedure with four generic parameters.
TFunc<TResult> A function with no parameters and a generic return type.
TFunc<T, TResult> A function with one generic parameter and a generic return type.
TFunc<T1, T2, TResult> A function with two generic parameters and a generic return type.
TFunc<T1, T2, T3, TResult> A function with three generic parameters and a generic return type.
TFunc<T1, T2, T3, T4, TResult> A function with four generic parameters and a generic return type.
TPredicate<T> A function with one generic parameter and a boolean return type.  This is used by collections as a “filter” for elements.  Elements that are passed to a predicate function which return false are excluded from the result.

 

The delegate types in SysUtils are interesting if you know how they are used.  They are probably just going in one ear and out the other if you don’t.  These delegates are often the prototypes for anonymous methods.  These are methods that can be declared in place and passed as parameters to other methods, but they are not pointers to methods of another object or class (although they can be).  You can create your own methods that take one of these types as a parameter, call that method and do something with the result.  This is a means of passing some control back to the caller, as they provide the code you’re calling.  I expected these function prototypes to be used as predicates on many of the new generic collection types… but I find that there are not used anywhere.  This is strange, but for now, it simplifies our discussion.  You will not need to know anything about the declarations in SysUtils.pas to use generics in Delphi.

 

 

What is Covariance and Contravariance?

Covariance is the ability to implicitly convert a value from a more specific type to an ancestor type.

Contravariance is the ability to implicitly convert a value from a less specific type to a derived type.

 

Generic Covariance and Contravariance

It is often disappointing to a user who begins using generics to find that covariance and contravariance is not supported in their implementation.  They are not supported in Delphi for native Windows.  Interestingly, they are supported in Prism on the .NET 3.5 framework although they aren’t available in C# until version 3.0 comes out on the .NET 4.0 platform.

At first it would seem that a method with a parameter of type TList<TStrings> could accept a value of type TList<TStringlist>.  Because TList is compatible with TList and TStrings is an ancestor of TStringList, it would seem that TList<TStringList> derives from TList<TStrings> but it doesn’t.  The type system is actually comparing TList<T1> to TList<T2>, which are incompatible types. 

The expression TList<T> is not to be thought of as a composition of 2 types with two sets of type information to be compared to another two sets of type information when determining compatibility. TList<T> is a single type with a single set of type information.  Hence, compatibility between T1 and T2 is not applicable…  To make such type comparisons would require covariance and contravariance.

 

Generics in older versions of Delphi (Templates)

Generics were introduced in Delphi 2009.  However, with some effort, you can achieve something similar in older versions.  Delphi can be tricked into building a sort of untyped template with some compromise in readability. 

The article titled “Templates in Object Pascal” by Rossen Assenov describes how to do this.  The article can currently be found here.

http://edn.embarcadero.com/article/27603

Basically, you write your template in two parts, an interface and an implementation.  Then in the consuming unit, you declare a type alias for your “type parameter” (the type placeholder you put in your template) and then immediately add an include directive for the interface snippet.  In the implementation section you add an include directive for the implementation snippet to finish it up.  This has the limitation that the template can only be consumed once per unit, but that’s a small price to pay for some of the benefits you might reap.

 

Phil Gilmore (www.interactiveasp.net)

Comments

# Delphi collection | Babystepsandbe said on January 23, 2013 10:04 AM:

Pingback from  Delphi collection | Babystepsandbe

# Impossible: Property using Generics in Delphi. Interfaces/classes/methods/fields/constraints/type inferencing are « The Wiert Corner – irregular stream of stuff said on January 29, 2013 10:01 PM:

Pingback from  Impossible: Property using Generics in Delphi. Interfaces/classes/methods/fields/constraints/type inferencing are « The Wiert Corner – irregular stream of stuff