Using Generics in Delphi
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
function TMyData<T>.Add(const aExistingSet: TGenericArray<T>;
const aNewValue: T): TGenericArray<T>;
begin
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
procedure TMyData<T>.ParameterizedMethod<T2>(const aGenericParameter: T2);
begin
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
procedure TMyData1<T>.WhatCanItDo(const aParm: T);
var
p: pointer;
obj: TObject;
begin
obj := TObject(aParm);
p := Pointer(TObject(aParm));
end;
procedure TMyData2<T>.WhatCanItDo(const aParm: T);
var
intf: IMyInterface;
conc: TMyConcreteClass;
begin
conc := TMyConcreteClass(aParm);
intf := TMyConcreteClass(aParm) as IMyInterface;
end;
function TMyData3<T>.WhatCanItDo(const aParm: T): T;
begin
result := T.Create;
end;
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
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)