I need to store a temporary list of records and was thinking that a TList
would be a good way to do this? However I am unsure how to do this with a TList
and was wondering if this is the best was and also if anyone has any examples of how to do this?
-
20Nobody has explicitly suggested `Generics.Collections.TList
`. Worth considering in my view. – David Heffernan Apr 27 '11 at 02:42 -
@David: That's definately the way to go. I've found Generics.Collections.TList
is WAY faster if it's a large read-only list of records. – Giel Apr 27 '11 at 09:22 -
2@David: I suggested it. :-) There are problems still with by-reference and by-value semantics, that would cause a lot of data copying, when adding stuff to the Generics list, though. @Giel - Yes, for a read-only list of records, it's GREAT, unless you needed to "copy in" a whole lot of records from somewhere else. Then the data-copy penalty might hurt you. – Warren P Apr 27 '11 at 14:30
-
@Warren All depends on how big the records are. – David Heffernan Apr 27 '11 at 14:33
-
@David: Yep. 4 byte record = No problem. :-) – Warren P Apr 27 '11 at 15:00
-
If you need to access to a record in generic TList by-reference and do not copy record: use List.List - direct access to array: lList := TList
.Create; [...] lRecP := @lList.List[i]; (lRecP: PTestRec; ) – alitrun Dec 26 '19 at 16:58
8 Answers
The easiest way is to create your own descendant of TList
. Here's a quick sample console app to demonstrate:
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes;
type
PMyRec=^TMyRec;
TMyRec=record
Value: Integer;
AByte: Byte;
end;
TMyRecList=class(TList)
private
function Get(Index: Integer): PMyRec;
public
destructor Destroy; override;
function Add(Value: PMyRec): Integer;
property Items[Index: Integer]: PMyRec read Get; default;
end;
{ TMyRecList }
function TMyRecList.Add(Value: PMyRec): Integer;
begin
Result := inherited Add(Value);
end;
destructor TMyRecList.Destroy;
var
i: Integer;
begin
for i := 0 to Count - 1 do
FreeMem(Items[i]);
inherited;
end;
function TMyRecList.Get(Index: Integer): PMyRec;
begin
Result := PMyRec(inherited Get(Index));
end;
var
MyRecList: TMyRecList;
MyRec: PMyRec;
tmp: Integer;
begin
MyRecList := TMyRecList.Create;
for tmp := 0 to 9 do
begin
GetMem(MyRec, SizeOf(TMyRec));
MyRec.Value := tmp;
MyRec.AByte := Byte(tmp);
MyRecList.Add(MyRec);
end;
for tmp := 0 to MyRecList.Count - 1 do
Writeln('Value: ', MyRecList[tmp].Value, ' AByte: ', MyRecList[tmp].AByte);
WriteLn(' Press Enter to free the list');
ReadLn;
MyRecList.Free;
end.
This eliminates a couple of things:
- It handles freeing the memory.
- You don't have to typecast everything to use it.
As Remy and Warren both said, it's a little more work because you have to allocate the memory when you add new records.

- 123,280
- 14
- 225
- 444
-
1You still have to allocate memory yourself manually, using GetMem, everywhere. If someone was to declare a record type on a heap somewhere, and add it to a MyRecList, you would get into trouble. Because this would get the obvious beginner into trouble I am down voting this very clever and interesting piece of code. – Warren P Apr 27 '11 at 13:31
-
1@Warren: I pointed that out in my last paragraph, so your point is moot. You've made your opinion clear in several posts here. Downvoting me because I disagreed with you seems pretty petty, but whatever. – Ken White Apr 27 '11 at 13:48
-
1It's about thinking about the new kids. Not petty. You pointed out more work, but not the safety aspects of record types and the stack/heap thing. – Warren P Apr 27 '11 at 14:29
-
1What will happen if user executes `MyRecList.Delete(i)` ? The right way is to override `Delete` method or to catch `Notify(cnRemoved)`. Moreover if the record contains dynamic-type fields (dynamic arrays, strings), you'll have to `Finalize` it before freeing to avoid memory leak. – Fr0sT Mar 06 '14 at 06:45
-
@Fr0sT: Where did I say I'd written a full implementation of everything that would be needed in a real-world application? I posted an **example**, and examples are intended to be small to illustrate a point. It's typical to leave out most error handling and only implement what's needed for that example. If you want me to write a full implementation for you along with unit tests, contact me and I'll send you my hourly rate. :-) – Ken White Mar 06 '14 at 11:59
-
2@KenWhite all I wanted to say is that your example teaches children bad things ;) – Fr0sT Mar 06 '14 at 14:59
-
1To avoid allocating memory out of this class one might add (if possible) an `Add` method (overload) with parameters of all the record fields and allocate that record inside such method. Of course, this would be usable only for records with a few fields. – TLama Sep 02 '14 at 15:58
First, if you want to combine a classic TList with Records, you will need to:
- Allocate your records on the heap, not on the stack. Use GetMem as Remy did.
- Take the address of the record and add it to the TList.
- When removing an item from the list, and using it, dereference it:
- Remember to free and clean up, afterwards.
Combining Lists with Records requires so much "pointers-and-heap-management" work that such a technique would be only within the capabilities of an expert.
Alternatives to what you have asked for that still use something called "TList", include using a generics.collections style TList, with Record types, which would have all the benefits of TList, but would require you to basically do a lot of entire-record-copies to get data into it.
The most idiomatic Delphi ways to do what you ask are to either:
use a TList or TObjectList with a Class Types instead of a record. Usually you end up subclassing either TList or TObjectList in this case.
Use a dynamic Array of Record Types, but be aware that it's harder to sort an Array type, and that expanding an array type at runtime isn't as speedy as it is with a TList.
Use generics.Collections TList with your classes. This lets you avoid subclassing TList or TObjectList each time you want to use a list with a different class.
A code sample showing Dynamic arrays:
TMyRec = record
///
end;
TMyRecArray = array of TMyRec;
procedure Demo;
var
myRecArray:TMyRecArray;
begin
SetLength(myRecArray,10);
end;
Now for some background information on why TList is not easy to use with Record types:
TList is better suited for use with Class types, because a variable of type 'TMyClass', where 'type TMyClass = class .... end;' can be easily "referred to" as a pointer value, which is what TList holds.
Variables of type Record are value-Types in Delphi, whereas class values are implicitly by-reference values. You can think of by-reference values as stealth-pointers. You don't have to dereference them to get at their contents, but when you add it to a TList, you're actually just adding a pointer to the TList, not making a copy or allocating any new memory.
The answer by Remy shows you literally you how to do exactly what you want, and I am writing my answer only because I want to warn you about the details of what you are asking, and suggest that you consider alternatives too.

- 65,725
- 40
- 181
- 316
-
1@Warren: The problem with arrays is that they aren't as easily sorted. I agree with the class type recommendation, though; you can more easily manage them with TObjectList. (BTW, the ^ is almost entirely optional now; the compiler handles most of that for you and you don't need it.) – Ken White Apr 26 '11 at 23:16
-
@Warren: Dynamic arrays are also not as memory-efficient as TList when it comes to adding and removing items to/from the list. TList grows exponentially, and removes efficiently. Arrays do not. You would have to implement those operatons manually when using arrays. – Remy Lebeau Apr 26 '11 at 23:46
-
1@Warren: The second problem with your answer (as I read it more carefully) is that it doesn't answer the actual question asked. It clearly demonstrates your feelings about pointers and records, but doesn't address the question "How do I store records in a TList". Sorry; I have to down-vote here. If I post the question "I cut my finger off with a kitchen knife while chopping vegetables. How do I stop the bleeding before I bleed out?", your answer shouldn't be "You should be more careful with a knife so you don't cut yourself.". Answer the question, and then post your opinions on other options. – Ken White Apr 27 '11 at 00:12
-
1He was thinking of Tlist. Not married with 2 kids with it. Maybe pedantic works for you. Real amd solid and safe answers that try to do something the user asks are not mere opinion. – Warren P Apr 27 '11 at 11:28
-
@Warren: Has nothing to do with pedantry. A specific question was asked. Having your opinion is great, but doesnt' make it an answer to the question if it isn't. As I said, answer the question first, and then offer alternative (and possibly better) solutions. – Ken White Apr 27 '11 at 12:51
-
4I have edited, and removed most of my opinions. I have instead pointed at what is commonly done in this language and environment, and what is not commonly done, and why. – Warren P Apr 27 '11 at 13:22
-
1Another example of a user doing exactly what I did is here, and I happen to agree that this user tried to educate the person answering the question. Note the upvotes. Would you downvote this guy Ken: http://stackoverflow.com/questions/5774598/declare-public-global-variable-in-delphi/5774686#5774686 – Warren P Apr 27 '11 at 13:33
-
@Warren: Relating to the post you linked, no, I would not downvote you there. You answered the question first, and then talked about alternatives. You didn't do that here prior to your edit, and thus my downvote - actually, you didn't do that here *after* your edit. You just said "here's a lot of things you'll need to figure out", and then "now here's to do it my way instead". IOW, you **still** didn't answer the question asked. – Ken White Apr 27 '11 at 19:35
-
1Well, your answer is accepted, and that's fine. I think that my answer provides a valuable alternative, and as I said, I didn't think it was worth duplicating what Remy did. (You did, however think that, and your answer is accepted, which is fine too.) – Warren P Apr 27 '11 at 19:42
-
I didn't duplicate what Remy did, unless Remy created a TList descendant that eliminated all of the typecasting and pointer dereferencing, and automatically deallocated the memory in the list. Whatever, Warren. On this we agree to disagree. Time to end this. – Ken White Apr 27 '11 at 21:02
You can take a look at our TDynArray wrapper. It's defined in an Open Source unit, working from Delphi 6 up to XE.
With TDynArray
, you can access any dynamic array (like TIntegerDynArray = array of integer
or TRecordDynArray = array of TMyRecord
) using TList
-like properties and methods, e.g. Count, Add, Insert, Delete, Clear, IndexOf, Find, Sort
and some new methods like LoadFromStream, SaveToStream, LoadFrom
and SaveTo
which allow fast binary serialization of any dynamic array, even containing strings or records - a CreateOrderedIndex
method is also available to create individual index according to the dynamic array content. You can also serialize the array content into JSON, if you wish. Slice, Reverse
or Copy
methods are also available.
It will handle a dynamic array of records, and even records within records, with strings or other dynamic arrays inside.
When using an external Count
variable, you can speed up a lot the adding of elements in the referred dynamic array.
type
TPerson = packed record
sCountry: string;
sFullName: string;
sAddress: string;
sCity: string;
sEmployer: string;
end;
TPersons = array of TPerson;
var
MyPeople: TPersons;
(...)
procedure SavePeopleToStream(Stream: TMemoryStream);
var aPeople: TPerson;
aDynArray: TDynArray;
begin
aDynArray.Init(TypeInfo(TPersons),MyPeople);
aPeople.sCountry := 'France';
aPeople.sEmployer := 'Republique';
aDynArray.Add(aPeople);
aDynArray.SaveToStream(Stream);
end; // no try..finally Free needed here
There is also a TDynArrayHashed
class, which allow internal hashing of a dynamic array content. It's very fast and able to hash any kind of data (there are standard hashers for strings, but you can supply your own - even the hash function can be customized).
Note that TDynArray
and TDynArrayHashed
are just wrappers around an existing dynamic array variable. You can therefore initialize a TDynArray
wrapper on need, to access more efficiently any native Delphi dynamic array.

- 42,305
- 3
- 71
- 159
-
@Ken White (by anticipation :) ) Yes, this is not a true `TList` implementation, I know. But it's definitively a `TList`-like implementation, faster than using a `TList` (since records are allocated by chunk). `TList` is NOT meant to store records, but pointers. And in our wrapper, there are methods you don't have with a `TList`, like hashing, internal saving or loading to memory or stream, sort with an external integer look-up index, `Slice` methods, and such. We use this e.g. in our framework for instance to store a cache of compiled SQL statements, in a few lines of code. – Arnaud Bouchez Apr 27 '11 at 05:47
-
Can only this wrapper be downloaded without the whole framework? – john_who_is_doe Mar 20 '19 at 10:46
-
You can extract the needed units: `SynCommons.pas` and `SynLZ.pas`, plus `Synopse.inc`. – Arnaud Bouchez Mar 25 '19 at 20:23
You can use TList for that, eg:
type
pRec = ^sRec;
sRec = record
Value: Integer;
...
end;
var
List: TList;
Rec: pRec;
I: Integer;
begin
List := TList.Create;
try
for I := 1 to 5 do begin
GetMem(Rec);
try
Rec^.Value := ...;
...
List.Add(Rec);
except
FreeMem(Rec);
raise;
end;
end;
...
for I := 0 to List.Count-1 do
begin
Rec := pRec(List[I]);
...
end;
...
for I := 0 to List.Count-1 do
FreeMem(pRec(List[I]));
List.Clear;
finally
List.Free;
end;
end;

- 555,201
- 31
- 458
- 770
-
2Note that in this case, Remy is solving the by-Value and by-reference semantic gap by having Rec be declared as pRec, which is a pointer type. I would argue he should call that variable PointerToARecord, or something. Most delphi programmers prefer to avoid directly using pointers where possible, thus the newer TList
and the classic dynamic-array features of Delphi are usually preferable. – Warren P Apr 26 '11 at 23:06 -
6@Warren: Delphi convention says that `PMyRec` means `pointer to TMyRec`. Read any of the RTL/VCL that uses pointers to see examples. Calling it `PointerToARecord` is redundant and long-winded. We get that you don't like pointers; that doesn't mean they don't have a place in the language. – Ken White Apr 27 '11 at 00:06
-
1I would use New and Dispose instead of GetMem, FreeMem. The poster could be using strings, interfaces or some other lifetime managed variable in his records. – The_Fox Apr 27 '11 at 06:49
-
2It's nothing to do with don't like. Pointers and newbies don't mix. As johan said. You're showing the newby a lawnmower and you're asking him to put his hand under the deck while it's running. – Warren P Apr 27 '11 at 13:14
Use Generiс TList from System.Generics.Collections. If you need to access to a record in generic TList by-reference and do not copy record: use List.List - direct access to the array of TList.
MyList := TList<TTestRec>.Create;
[...]
var lRecP: PTestRec; // (PTestRec = ^TTestRec)
lRecP := @MyList.List[i];
Now you can access to record inside Tlist array without copying it.

- 1,127
- 8
- 14
If using an older version of Delphi where generics isn't present, consider inheriting from TList and override Notify method. When adding an item, alloc memory, copy added pointer memory content and override content in list. When removing, just free memory.
TOwnedList = class(TList)
private
FPtrSize: integer;
protected
procedure Notify(Ptr: Pointer; Action: TListNotification); override;
public
constructor Create(const APtrSize: integer);
end;
constructor TOwnedList.Create(const APtrSize: integer);
begin
inherited Create();
FPtrSize := APtrSize;
end;
procedure TOwnedList.Notify(Ptr: Pointer; Action: TListNotification);
var
LPtr: Pointer;
begin
inherited;
if (Action = lnAdded) then begin
GetMem(LPtr, FPtrSize);
CopyMemory(LPtr, Ptr, FPtrSize); //May use another copy kind
List^[IndexOf(Ptr)] := LPtr;
end else if (Action = lnDeleted) then begin
FreeMem(Ptr, FPtrSize);
end;
end;
Usage:
...
LList := TOwnedList.Create(SizeOf(*YOUR RECORD TYPE HERE*));
LList.Add(*YOU RECORD POINTER HERE*);
...
- Note that where I did use CopyMemory(LPtr, Ptr, FPtrSize), you may use another copy aproach. My list is intended to store a record with pointer references, so it doesn't manage it's fields memory.

- 134
- 11
It all depends on the type of data you want to store.
You might consider using TCollection
and TCollectionItem
.
Here is (edited) code from a working unit, in which I used TCollection
to read a list of report definitions from a folder. Each report consisted of a sort of template and an SQL statement which had to be stored together with a file name.
Since it is edited, and uses some of my own units (TedlFolderRtns reads files into an internal list, to name but one), the example is simple enough to be useful. With a few replace all, you can adapt to whatever your need.
Look up TCollection in the help, you can do a lot with it. And it keeps your code handling nicely grouped together in a class-like structure.
unit cReports;
interface
uses
SysUtils, Classes, XMLDoc, XMLIntf, Variants,
// dlib - Edelcom
eIntList, eProgSettings,eFolder ;
type
TReportDefItem = class(TCollectionItem)
private
fSql: string;
fSkeleton: string;
fFileName: string;
procedure Load;
procedure SetFileName(const Value: string);
public
constructor Create(Collection:TCollection); override;
destructor Destroy ; override;
property FileName: string read fFileName write SetFileName;
property Sql : string read fSql write fSql;
property Skeleton : string read fSkeleton write fSkeleton;
end;
TReportDefList = class(TCollection)
private
function OsReportFolder: string;
function GetAction(const Index: integer): TReportDefItem;
public
constructor Create(ItemClass: TCollectionItemClass);
destructor Destroy; override;
procedure LoadList;
function Add : TReportDefItem;
property Action [ const Index:integer ]: TReportDefItem read GetAction;
end;
implementation
{ TReportDefList }
constructor TReportDefList.Create(ItemClass: TCollectionItemClass);
begin
inherited;
end;
destructor TReportDefList.Destroy;
begin
inherited;
end;
function TReportDefList.Add: TReportDefItem;
begin
Result := TReportDefItem( Add() );
end;
function TReportDefList.GetAction(const Index: integer): TReportDefItem;
begin
if (Index >= 0) and (Index < Count)
then Result := TReportDefItem( Items[Index] )
else Result := Nil;
end;
procedure TReportDefList.LoadList;
var Folder : TedlFolderRtns;
i : integer;
Itm : TReportDefItem;
begin
Folder := TedlFolderRtns.Create;
try
Folder.FileList( OsReportFolder,'*.sw.xml', False);
for i := 0 to Folder.ResultListCount -1 do
begin
Itm := Add();
Itm.FileName := Folder.ResultList[i];
end;
finally
FreeAndNil(Folder);
end;
end;
function TReportDefList.OsReportFolder: string;
begin
Result := Application.ExeName + '_RprtDef';
end;
{ TReportDefItem }
constructor TReportDefItem.Create(Collection: TCollection);
begin
inherited;
fSql := '';
fSkeleton := '';
end;
destructor TReportDefItem.Destroy;
begin
inherited;
end;
procedure TReportDefItem.Load;
var XMLDoc : IXMLDocument;
TopNode : IXMLNode;
FileNode : IXmlNode;
iWebIndex, iRemoteIndex : integer;
sWebVersion, sRemoteVersion: string;
sWebFileName: string;
begin
if not FileExists(fFileName ) then Exit;
XMLDoc := TXMLDocument.Create(nil);
try
XMLDoc.LoadFromFile( fFileName );
XMLDoc.Active := True;
TopNode := XMLDoc.ChildNodes.FindNode('sw-report-def');
if not Assigned(TopNode) then Exit;
FileNode := TopNode.ChildNodes.First;
while Assigned(FileNode) do
begin
fSql := VarToStr( FileNode.Attributes['sql'] );
fSkeleton := VarToStr( FileNode.Attributes['skeleton'] );
FileNode := FileNode.NextSibling;
end;
XMLDoc.Active := False;
finally
XMLDoc := Nil;
end;
end;
procedure TReportDefItem.SetFileName(const Value: string);
begin
if fFileName <> Value
then begin
fFileName := Value;
Load;
end;
end;
end.
Use as :
fReports := TReportDefList.Create( TReportDefItem );
fReports.LoadList();

- 5,038
- 8
- 44
- 61
-
Seems like a good answer, except the item is a class, not a record as per the OP. +1 since it still seems to useful. – Reversed Engineer Apr 11 '19 at 12:57
We've just run into a similar issue here with a generic list of records. Hope the following psuedo code helps.
type
PPat = ^TPat;
TPat = record
data: integer;
end;
...
var
AList: TList<PPat>;
...
procedure TForm1.Button1Click(Sender: TObject);
var
obj: PPat;
begin
obj := AList[0];
obj.data := 1;
Assert(obj.data = AList[0].data); // correct
end;
procedure TForm1.FormCreate(Sender: TObject);
var
obj: PPat;
begin
AList := TList<PPat>.Create;
GetMem(obj, SizeOf(TPat)); // not shown but need to FreeMem when items are removed from the list
obj.data := 2;
AList.Add(obj);
end;

- 1,573
- 3
- 14
- 19