【问题标题】:Delphi TList of recordsDelphi TList 的记录
【发布时间】:2011-08-13 10:23:05
【问题描述】:

我需要存储一个临时的记录列表,并认为TList 会是一个好方法吗?但是我不确定如何使用TList 来执行此操作,并且想知道这是否是最好的,以及是否有人有任何示例说明如何执行此操作?

【问题讨论】:

  • 没有人明确建议Generics.Collections.TList<T>。在我看来值得考虑。
  • @David:这绝对是要走的路。我发现如果 Generics.Collections.TList 是一个大型的只读记录列表,它会更快。
  • @David:我建议的。 :-) 但是,在向泛型列表中添加内容时,按引用和按值语义仍然存在问题,这会导致大量数据复制。 @Giel - 是的,对于只读记录列表来说,这很棒,除非您需要从其他地方“复制”大量记录。那么数据复制惩罚可能会伤害你。
  • @Warren 一切都取决于记录的大小。
  • @David:是的。 4 字节记录 = 没问题。 :-)

标签: delphi record tlist


【解决方案1】:

最简单的方法是创建自己的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 所说,这需要更多的工作,因为您必须在添加新记录时分配内存。

【讨论】:

  • 您仍然需要自己手动分配内存,使用 GetMem,无处不在。如果有人要在某处的堆上声明一个记录类型,并将其添加到 MyRecList,你会遇到麻烦。因为这会给明显的初学者带来麻烦,所以我对这段非常聪明和有趣的代码投了反对票。
  • @Warren:我在上一段中指出了这一点,所以你的观点没有实际意义。你已经在这里的几篇文章中表达了你的观点。因为我不同意你而对我投反对票似乎很微不足道,但无论如何。
  • 这是关于新孩子的思考。不小气。您指出了更多工作,但没有指出记录类型和堆栈/堆的安全方面。
  • @KenWhite 我只想说你的例子教孩子们坏事;)
  • 为了避免从此类中分配内存,可以添加(如果可能)带有所有记录字段参数的Add 方法(重载)并在此类方法中分配该记录。当然,这仅适用于具有少数字段的记录。
【解决方案2】:

首先,如果您想将经典的 TList 与 Records 结合起来,您需要:

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

将列表与记录相结合需要大量的“指针和堆管理”工作,因此这种技术只能在专家的能力范围内。

您所要求的替代方案仍然使用称为“TList”的东西,包括使用带有记录类型的 generics.collections 样式 TList,这将具有 TList 的所有优点,但基本上需要您做很多事情整个记录副本以将数据放入其中。

按照您的要求做的最惯用的 Delphi 方法是:

  1. 使用带有类类型的 TList 或 TObjectList 而不是记录。在这种情况下,通常您最终会继承 TList 或 TObjectList。

  2. 使用记录类型的动态数组,但请注意,对数组类型进行排序比较困难,并且在运行时扩展数组类型不如使用 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 类型一起使用的背景信息:

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

Record 类型的变量在 Delphi 中是值类型,而类值是隐式引用值。您可以将按引用值视为隐形指针。您不必取消对它们的引用来获取它们的内容,但是当您将它添加到 TList 时,您实际上只是添加了指向 TList 的指针,而不是复制或分配任何新内存。

Remy 的回答从字面上告诉你如何完全做你想做的事,我写我的答案只是因为我想警告你关于你所问的细节,并建议你也考虑替代品。

【讨论】:

  • @Warren:数组的问题在于它们不容易排序。不过,我同意班级类型的建议;您可以使用 TObjectList 更轻松地管理它们。 (顺便说一句,^ 现在几乎完全是可选的;编译器会为您处理大部分内容,而您不需要它。)
  • @Warren:您的回答的第二个问题(我更仔细地阅读了)是它没有回答实际提出的问题。它清楚地展示了您对指针和记录的感受,但没有解决“如何在 TList 中存储记录”的问题。对不起;我必须在这里投反对票。如果我发布问题“我在切菜时用菜刀割断了手指。如何在我流血之前止血?”,你的回答不应该是“你应该更小心用刀,这样你就不会不要割伤自己。”。回答问题,然后发表您对其他选项的意见。
  • 他在想Tlist。没有结婚有2个孩子。也许迂腐对你有用。尝试做用户要求的事情的真实、可靠和安全的答案不仅仅是意见。
  • 我已编辑并删除了我的大部分意见。相反,我指出了在这种语言和环境中通常会做什么,不经常做什么,以及为什么。
  • 另一个与我完全一样的用户示例在这里,我碰巧同意该用户试图教育回答问题的人。注意赞成票。你会否决这个家伙肯:stackoverflow.com/questions/5774598/…
【解决方案3】:

您可以查看我们的TDynArray wrapper。它在一个开源单元中定义,从 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 方法也可用。

它将处理记录的动态数组,甚至记录中的记录,其中包含字符串或其他动态数组。

当使用外部Count 变量时,您可以加快在引用的动态数组中添加元素的速度。

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 (by 预期 :) ) 是的,这不是真正的 TList 实现,我知道。但它绝对是一个类似TList 的实现,比使用TList 更快(因为记录是按块分配的)。 TList 不是用来存储记录,而是指针。在我们的包装器中,有一些TList 没有的方法,例如散列、内部保存或加载到内存或流、使用外部整数查找索引排序、Slice 方法等。我们使用这个,例如例如,在我们的框架中存储已编译 SQL 语句的缓存,只需几行代码。
  • 没有整个框架就只能下载这个wrapper吗?
  • 可以提取需要的单位:SynCommons.pasSynLZ.pas,加上Synopse.inc
【解决方案4】:

您可以为此使用 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;

【讨论】:

  • 请注意,在这种情况下,Remy 通过将 Rec 声明为指针类型 pRec 来解决按值和按引用的语义差距。我认为他应该调用该变量 PointerToARecord 或其他名称。大多数 Delphi 程序员喜欢尽可能避免直接使用指针,因此较新的 TList 和 Delphi 的经典动态数组特性通常更可取。
  • @Warren:Delphi 约定说PMyRec 表示pointer to TMyRec。阅读任何使用指针的 RTL/VCL 以查看示例。称它为PointerToARecord 是多余且冗长的。我们知道你不喜欢指针;这并不意味着他们在语言中没有一席之地。
  • 我会使用 New 和 Dispose 而不是 GetMem、FreeMem。发帖人可能在他的记录中使用字符串、接口或其他一些生命周期管理变量。
  • 跟不喜欢没关系。指针和新手不要混在一起。正如约翰所说。您正在向割草机展示新的,并要求他在运行时将手放在甲板下。
【解决方案5】:

使用 System.Generics.Collections 中的 Generiс TList。 如果您需要通过引用访问通用 TList 中的记录并且不要复制记录: 使用 List.List - 直接访问 TList 的数组。

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

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

现在您无需复制即可访问 Tlist 数组中的记录。

【讨论】:

    【解决方案6】:

    这完全取决于您要存储的数据类型。

    您可以考虑使用 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,因为它似乎仍然有用。
    【解决方案7】:

    我们刚刚遇到了一个类似的问题,其中包含一个通用的记录列表。希望以下伪代码有所帮助。

    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;
    

    【讨论】:

      【解决方案8】:

      如果使用不存在泛型的旧版 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) 的地方,您可以使用另一种复制方法。我的列表旨在存储带有指针引用的记录,因此它不管理它的字段内存。

      【讨论】:

        猜你喜欢
        • 2023-03-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-13
        相关资源
        最近更新 更多