Delphi TList记录列表

38

我需要存储一个临时的记录列表,考虑使用 TList 进行实现,但我不确定如何使用 TList 进行操作,想知道是否这种方式最好,并了解一些如何实现的示例。


20
在我看来,没有人明确建议使用Generics.Collections.TList<T>。我认为这值得考虑。 - David Heffernan
@David:这绝对是正确的方法。我发现如果是大型只读记录列表,Generics.Collections.TList<T> 的速度要快得多。 - Giel
2
@David:我建议的。:-) 尽管如此,通过引用和按值语义仍存在问题,这将导致在向泛型列表添加内容时进行大量数据复制。@Giel - 是的,对于只读记录列表来说,它非常好,除非您需要从其他地方“复制”大量记录。然后,数据复制惩罚可能会伤害您。 - Warren P
@Warren 一切取决于记录有多大。 - David Heffernan
@David:没错,4字节记录=没问题。 :-) - Warren P
如果您需要通过引用访问通用 TList 中的记录并且不复制记录:请使用 List.List - 直接访问数组:lList := TList<TTestRec>.Create; [...] lRecP := @lList.List[i]; (lRecP: PTestRec;) - alitrun
8个回答

28
最简单的方法是创建自己的 TList 子类。这里有一个快速的示例控制台应用程序以进行演示:
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.

这样可以消除一些问题:

  • 它处理了内存的释放。
  • 你不需要为使用它而将所有东西都进行类型转换。

正如Remy和Warren都说的那样,这会多做一点工作,因为你需要在添加新记录时分配内存。


1
你仍然需要手动分配内存,到处使用GetMem。如果有人在堆上声明一个记录类型,并将其添加到MyRecList中,那么你会遇到麻烦。因为这会让显而易见的初学者陷入困境,所以我会对这段非常聪明和有趣的代码进行反对投票。 - Warren P
1
@Warren:我在上一个段落中指出了这一点,所以你的观点是无关紧要的。你已经在几篇帖子中表达了自己的意见。因为我不同意你而对我进行投票似乎相当小气,但随你便吧。 - Ken White
1
如果用户执行 MyRecList.Delete(i) 会发生什么?正确的方法是重写 Delete 方法或捕获 Notify(cnRemoved)。此外,如果记录包含动态类型字段(动态数组、字符串),则必须在释放之前对其进行 Finalize 处理,以避免内存泄漏。 - Fr0sT
2
@KenWhite,我想说的是,你的例子教孩子们坏东西 ;) - Fr0sT
1
为了避免在该类中分配内存,可以添加(如果可能)一个Add方法(重载),其参数为所有记录字段,并在此类方法中分配该记录。当然,这仅适用于具有少量字段的记录。 - TLama
显示剩余2条评论

21

首先,如果您想将经典的TList与记录结合使用,则需要:

  1. 在堆上分配记录,而不是在栈上。像Remy一样使用GetMem。
  2. 获取记录的地址并将其添加到TList中。
  3. 从列表中删除项目并使用它时,对其进行解引用:
  4. 记得最后释放和清理。

将列表与记录结合需要进行大量的“指针和堆管理”工作,这种技术只适用于专家。

您所要求的替代方案仍然使用名为“TList”的东西,包括使用泛型.collections样式的带有记录类型的TList,它将具有TList的所有好处,但需要您基本上执行整个记录复制才能将数据放入其中。

实现您所要求的最惯用的Delphi方法之一是:

  1. 使用具有类类型而非记录的TList或TObjectList。通常在这种情况下,您最终会子类化TList或TObjectList。

  2. 使用记录类型的动态数组,但请注意,对数组类型进行排序比使用TList更难,并且在运行时扩展数组类型的速度不如TList快。

  3. 使用generics.Collections TList与您的类。这样,每当您想要使用具有不同类的列表时,就可以避免子类化TList或TObjectList。

下面是一个展示动态数组的代码示例:

 TMyRec = record
    ///
 end;

 TMyRecArray = array of TMyRec;

 procedure Demo;
 var
    myRecArray:TMyRecArray;
 begin
    SetLength(myRecArray,10);
 end;

以下是关于为什么使用TList与Record类型不容易的一些背景信息:

对于Class类型,TList更适合使用,因为类类型的变量'TMyClass', 其中'type TMyClass = class .... end;' 可以很容易地被“引用”为一个指针值,这就是TList所持有的。

在Delphi中,Record类型的变量是Value-Types,而类值被隐式地作为by-reference值。你可以把by-reference值看作是隐形指针。你不需要解引用它们才能访问它们的内容,但当你将其添加到TList时,你实际上只是向TList添加了一个指针,而不是复制或分配任何新内存。

Remy的答案展示了如何精确地做到你想要的,我写我的答案只是因为我想警告你关于你所要求的细节,并建议你考虑其他替代方案。


1
@Warren:数组的问题在于它们不太容易排序。我同意使用类类型的建议,因为你可以更轻松地使用TObjectList进行管理。(顺便说一句,现在^几乎完全是可选的了;编译器会为你处理大部分内容,你不需要它。) - Ken White
1
@Warren:我仔细阅读后发现你回答的第二个问题是它并没有回答实际提问的问题。它清楚地展示了你对指针和记录的感受,但并没有解决“如何在TList中存储记录”的问题。抱歉,我必须在这里给你点个踩。如果我发布了这样一个问题:“我在切菜时用厨房刀割伤了手指,我应该如何止血以免失血过多?”,你的回答不应该是“你应该更加小心使用刀具,以免自己割伤。”。先回答问题,然后再发表你对其他选项的看法。 - Ken White
4
我已编辑并删除了大部分个人观点。相反,我指出了这种语言和环境中通常会做什么、不会做什么以及原因。 - Warren P
1
另一个用户做了和我一样的事情的例子在这里,我碰巧同意这个用户试图教育回答问题的人。请注意赞成票。你会对这个家伙Ken投反对票吗:https://dev59.com/9G025IYBdhLWcg3w1JeG#5774686 - Warren P
1
好的,你的答案已被接受,这很好。我认为我的答案提供了有价值的替代方案,正如我所说,我不认为复制Remy所做的工作是值得的。(然而,你认为值得,你的答案也被接受了,这也很好。) - Warren P
显示剩余5条评论

11
您可以查看我们的 TDynArray包装器。它定义在一个开源单元中,适用于从Delphi 6到XE的版本。
使用 TDynArray,您可以像使用 TList 那样访问任何动态数组(如 TIntegerDynArray = array of integerTRecordDynArray = array of TMyRecord),例如 Count、Add、Insert、Delete、Clear、IndexOf、Find、Sort 等属性和方法,还有一些新方法,例如 LoadFromStream、SaveToStream、LoadFromSaveTo,可快速地对任何动态数组进行二进制序列化,甚至包含字符串或记录 - 还提供了 CreateOrderedIndex 方法以根据动态数组内容创建个体索引。您还可以将数组内容序列化为 JSON,如果需要的话,也可以使用 Slice、ReverseCopy 方法。
它可以处理一个包含记录的动态数组,甚至可以处理内部包含字符串或其他动态数组的记录。
当使用外部的计数变量时,可以大大加快将元素添加到所引用的动态数组中的速度。
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

还有一个 TDynArrayHashed 类,它允许对动态数组内容进行内部哈希。它非常快速,并且能够哈希任何类型的数据(有用于字符串的标准哈希函数,但您也可以提供自己的哈希函数 - 甚至可以定制哈希算法)。

请注意,TDynArrayTDynArrayHashed 只是现有动态数组变量的包装器。因此,您可以根据需要初始化一个 TDynArray 包装器,以更有效地访问任何本机 Delphi 动态数组。


@Ken White(提前感谢 :))是的,我知道这不是真正的TList实现。但它绝对是类似于TList的实现,比使用TList更快(因为记录是按块分配的)。TList不适用于存储记录,而是指针。在我们的包装器中,有一些TList没有的方法,例如哈希、内部保存或加载到内存或流、使用外部整数查找索引进行排序、Slice方法等等。例如,在我们的框架中,我们使用几行代码来存储编译后的SQL语句缓存。 - Arnaud Bouchez
这个包装器能否单独下载而不需要整个框架? - john_who_is_doe
您可以提取所需的单元:SynCommons.pasSynLZ.pas,以及 Synopse.inc - Arnaud Bouchez

5
您可以使用TList来实现,例如:
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
请注意,在这种情况下,Remy 通过将 Rec 声明为指针类型 pRec 来解决按值和按引用的语义差异。我认为他应该将该变量命名为 PointerToARecord 或其他类似的名称。大多数 Delphi 程序员通常喜欢尽可能避免直接使用指针,因此较新的 TList<Generic> 和经典的 Delphi 动态数组功能通常更可取。 - Warren P
6
Delphi的约定规定PMyRec表示指向TMyRec的指针。阅读使用指针的RTL/VCL的任何代码即可看到示例。将其称为“PointerToARecord”是冗余且冗长的。我们知道你不喜欢指针;但这并不意味着它们在语言中没有用处。 - Ken White
1
我会使用New和Dispose而不是GetMem和FreeMem。发帖者可能在记录中使用字符串、接口或其他生命周期受控变量。 - The_Fox
2
这与不喜欢无关。指针与新手不搭配。正如约翰所说的那样。你正在向新手展示一个割草机,并要求他在机器运转时把手放在底盘下面。 - Warren P

4

使用 System.Generics.Collections 中的泛型 TList。如果需要通过引用来访问泛型 TList 中的记录且不想复制记录,请使用 List.List - 直接访问 TList 数组。

MyList := TList<TTestRec>.Create; 
[...] 

var lRecP: PTestRec; // (PTestRec = ^TTestRec)
lRecP := @MyList.List[i]; 

现在您可以访问Tlist数组内的记录而无需复制它。

2
如果使用旧版Delphi而没有泛型,考虑从TList继承并重写Notify方法。添加项时,分配内存,复制已添加指针的内存内容并覆盖列表中的内容。删除时,只需释放内存。最初的回答。
  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;

使用方法:

...
LList := TOwnedList.Create(SizeOf(*YOUR RECORD TYPE HERE*));
LList.Add(*YOU RECORD POINTER HERE*);
...
  • 需要注意的是,当我使用CopyMemory(LPtr,Ptr,FPtrSize)时,您可以使用其他复制方法。我的列表旨在存储带有指针引用的记录,因此它不管理其字段内存。
注:Original Answer翻译成“最初的回答”

1

我们在这里遇到了一个类似的问题,涉及到记录的通用列表。希望以下伪代码能够帮助解决。

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

一切都取决于您想要存储的数据类型。

您可以考虑使用TCollectionTCollectionItem

这是我从一个工作单元编辑而来的代码。我在其中使用TCollection从文件夹中读取报表定义列表。每个报表由一种模板和一个SQL语句组成,必须与文件名一起存储。

由于它已经过编辑,并且使用了一些我自己的单元(例如TedlFolderRtns将文件读入内部列表),因此该示例足够简单易懂。通过几次替换,您就可以根据自己的需要进行调整。

在帮助文档中查找TCollection,您可以用它做很多事情。它可以将您的代码处理得像一个类似结构。

  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.

用法如下:

fReports := TReportDefList.Create( TReportDefItem );
fReports.LoadList();

看起来是一个不错的答案,只是该项是一个类,而不是根据 OP 的记录。+1 因为它仍然似乎有用。 - Reversed Engineer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接