35
votes

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?

8
Nobody has explicitly suggested Generics.Collections.TList<T>. Worth considering in my view.David Heffernan
@David: That's definately the way to go. I've found Generics.Collections.TList<T> is WAY faster if it's a large read-only list of records.Giel
@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
@Warren All depends on how big the records are.David Heffernan
@David: Yep. 4 byte record = No problem. :-)Warren P

8 Answers

27
votes

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.

19
votes

First, if you want to combine a classic TList with Records, you will need to:

  1. Allocate your records on the heap, not on the stack. Use GetMem as Remy did.
  2. Take the address of the record and add it to the TList.
  3. When removing an item from the list, and using it, dereference it:
  4. 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:

  1. 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.

  2. 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.

  3. 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.

11
votes

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.

4
votes

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;
2
votes

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
votes

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();
1
votes

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
votes

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.