Choosing Delphi Generics for Type‑Safe Collections in VCL and FireMonkey
Learn how Delphi generics provide type‑safe collections and algorithms with zero runtime overhead, see a reusable cache example, and verify specialization via the map file.
07 Nov 2025, 00:21 UTC

The problem: casting clutter in container code
When you need a list of strings or a dictionary that maps an integer ID to a record, the pre‑generics way in Delphi forced you to use TList or TDictionary with TObject as the element type. Every insertion required a cast, and every read‑back needed another cast, which made the code noisy and hid type errors until runtime.
Thesis: generics give you compile‑time safety without runtime overhead
Delphi generics, introduced in Delphi 2009, let you declare a container once and reuse it with any type. The compiler creates a specialized version for each concrete type argument, so the generated code is as efficient as a hand‑written, non‑generic class while keeping the source clean and type‑checked.
Where generics shine
- Type‑safe collections –
TList<T>,TDictionary<TKey,TValue>,TQueue<T>fromSystem.Generics.Collections. - Algorithms that work on any comparable type – by constraining a generic method with
IComparable<T>you can write a single sort routine that works for integers, strings, or custom records. - Reusable UI‑agnostic code – the same generic helper can be used in a VCL form, a FireMonkey frame, or a console utility because it contains no UI‑specific dependencies.
Worked example: a generic cache for VCL and FireMonkey
Suppose you need a simple cache that stores any value keyed by a string and automatically removes the oldest entry when a limit is reached. The cache itself does not depend on VCL or FireMonkey types, so it can be shared.
unit GenericCache;
interface
uses
System.Generics.Collections;
type
TCache<T> = class
private
FLimit: Integer;
FItems: TDictionary<string, T>;
FOrder: TList<string>; // tracks insertion order
public
constructor Create(ALimit: Integer);
procedure Add(const AKey: string; const AValue: T);
function TryGetValue(const AKey: string; out AValue: T): Boolean;
procedure Clear;
end;
implementation
{ TCache<T> }
constructor TCache<T>.Create(ALimit: Integer);
begin
FLimit := ALimit;
FItems := TDictionary<string, T>.Create;
FOrder := TList<string>.Create;
end;
procedure TCache<T>.Add(const AKey: string; const AValue: T);
begin
if FItems.ContainsKey(AKey) then
FItems[AKey] := AValue
else
begin
FItems.Add(AKey, AValue);
FOrder.Add(AKey);
if FOrder.Count > FLimit then
begin
var OldKey := FOrder[0];
FOrder.Delete(0);
FItems.Remove(OldKey);
end;
end;
end;
function TCache<T>.TryGetValue(const AKey: string; out AValue: T): Boolean;
begin
Result := FItems.TryGetValue(AKey, AValue);
end;
procedure TCache<T>.Clear;
begin
FItems.Clear;
FOrder.Clear;
end;
end.
To use the cache in a VCL form:
uses
GenericCache, Vcl.StdCtrls;
var
StringCache: TCache<string>;
procedure TForm1.FormCreate(Sender: TObject);
begin
StringCache := TCache<string>.Create(100);
StringCache.Add('welcome', 'Hello, Delphi!');
if StringCache.TryGetValue('welcome', var Value) then
ShowMessage(Value);
end;
The same unit can be dropped into a FireMonkey project; only the uses clause changes to FMX.Forms and FMX.StdCtrls if you need UI controls.
Trade‑off: binary size growth
Each distinct type argument triggers the compiler to emit a new copy of the generic class. If you instantiate TCache<Integer>, TCache<string>, TCache<TPerson>, and many others, the executable will contain separate method bodies for each. For most applications this increase is negligible, but in large libraries with dozens of specializations you should:
- Enable the map file (
Project → Options → Linker → Map file). - Build and open the
.mapfile. - Look for symbols like
TCache$Integer,TCache$String, etc., to see how many specializations were generated. - If the count is high and size matters, consider refactoring to a non‑generic base class with virtual methods, or limit the number of distinct type arguments.
How to verify that generics are working as expected
Follow these steps in any Delphi IDE (tested with Delphi 11 Alexandria, but the behavior is the same from Delphi 2009 onward):
- Create a new VCL Forms Application.
- Add a unit named
GenericCacheand paste the code from the example above. - In the form’s unit, add
uses GenericCache;and theFormCreatehandler shown. - Build the project (
Shift+F9). No compile errors should appear; note that no casts are needed when retrieving the cached value. - Run the application (
F9) and click the form; a message box showsHello, Delphi! - To confirm specialization, enable the map file, rebuild, and inspect the map for symbols such as
TCache$string.
If you change the type argument to Integer and repeat the steps, the compiler will generate a separate TCache$Integer specialization, and the cached value will be retrieved without any casting.
Actionable takeaway
Start by replacing any TList or TDictionary that stores TObject with the corresponding generic version from System.Generics.Collections. For reusable logic that does not depend on UI frameworks, write a generic class or method once and instantiate it with the concrete types you need. Keep an eye on the map file if you notice the executable growing unexpectedly, and consider consolidating rarely used specializations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.