Delphi实现开源Vector向量容器类

几周前,我在C ++ Builder上写了一篇文章,在那里我完全超出了我的舒适范围,并实现了我的第一个C ++类。

自从我对C ++ Builder产生了兴趣之后,在接下来的几周和几个月中,我将继续使用C / C ++进行冒险,但是与其他许多人一样,Corona病毒迫使我对工作进行不同的优先级排序。所以暂时我的焦点将是Delphi。

在这篇Delphi文章中,我想谈谈Vector向量容器,这是C ++中的一个概念,对于Delphi开发人员可能是陌生的。当您阅读“Vector向量”一词时,您可能会想到3D图形和矩阵数学。但是向量容器完全是另外一回事。

简而言之,向量容器是一个基于泛型的类,可简化您使用可表示为“值序列”的列表,数组,队列和数据的方式。这听起来可能不太令人兴奋,但请继续阅读,您可能会感到惊讶。

我必须承认,“载体”一词最初对于这样一个基本而扎根的话题似乎显得格格不入;但是,如果您用Google搜索该词的含义,则矢量仅表示方向标题从所有手段和目的来看,指针都是一个标题。

Delphi中的Vector向量

尽管在功能方面,Object Pascal和C ++在各方面实际上都是相同的,但是在某些方面,语言和生态系统是不同的;从根本上讲,在某些地方。Delphi可能没有与C ++标准库完全相同的报告,但是它始终为我们提供了实现相同目标的良好选择。Delphi和C ++ Builder在对VCL和FMX框架的支持上有共同点,但是vector类不是VCL或FMX的一部分,而是C ++标准库(std :: vector)。因此,Delphi实际上并没有直接匹配的东西。

因为我发现Vector向量容器在C ++中是如此有用,所以我想了-为什么不为Delphi创建Vector向量实现?而这正是我所做的。因此,在本文中,我将引导您了解Vector向量列表为何有用,如何使用它们-最后但并非最不重要的一点是,您可以在自己的项目中复制和使用的完整源代码(完全免费和开源,没有字符串)附件)。

注意:本文结尾​​处提供了BitBucket存储库链接。

集装箱,有什么大不了的?

如前所述,Vector向量类只是一个容器;处理“数据序列”的容器,在大多数情况下,这表示列表或数组。向量类之所以被称为容器,是因为它不涉及项目的分配或存储方式,而是将其留给了另一个称为分配器的类。

考虑向量类(纯粹作为协同作用或并行作用)的一种有用方法是考虑TDataset。TDataset可以抽象出数据的来源以及其在幕后的组织方式。就像TDataset一样,向量类提供了导航数据序列的方法,但是它自身并不包含细节。

由于向量列表实际上在C ++生态系统中的每个地方都被使用,因此它有助于减少您需要编写的代码量。在数组上使用的相同代码将适用于列表。开发人员还可以编写自己的分配器实例,将数据存储在磁盘上或将数据库记录直接映射到C结构。只要有人为该特定方案实现了分配器,向量容器就可以表示它。能够自己滚动,会带来一些有趣的可能性。

分配器类

C ++标准库开箱即用地提供了两个分配器类,这两个分配器类均表示可与类型化数据一起使用的内存中列表。还有上面我提到的第三个分配器类型(或从技术上讲,分配器继承自的基类),这意味着您可以提供自己的类型。总结一下:

  • 已排序
  • 动态
  • 自订

让我们看看它们中的每一个,看看有什么区别。

顺序分配器

序列化意味着您的项目列表被分配为一个连续的内存块。一个物品一个接一个地整齐地排列。在Delphi中最接近的东西是打包记录的数组。该分配器的优点是读取值异常快,因为它可以使用简单的数学计算每个项目的偏移量。

缺点是,在填充这样的向量列表时,必须非常频繁地重新分配内存。在我的谦虚实现中,我没有花时间实现缓存机制,这意味着每次添加,插入或删除项目时,都会重新分配保存该列表的整个内存段(我将在下一个修订版中添加一些内容) )。

但是,C ++规范在适当的缓存下运行,分配的项目比实际使用的多,因此将重新分配的数量降至最低。对于Delphi列表,相同的容量和使用系统也是常见的。

顺序分配器的另一个好处是,整个内容可以快速转储到磁盘或在内存中复制。毕竟所有项目都保存在单个内存中,从而可以预测大小和偏移量(因此您可以在单个操作中将数据写入TFileStream)。两个向量列表(具有相同分配器类型)之间的分配同样有效。

因此,尽管普通的编程任务可能不会立即从顺序存储中受益,但在某些低层场景中,Vector向量容器可以创造奇迹。

出于好奇,我可以提到在Nintendo DS等平台上,显示内容实际上是一个文件。因此,通过将值写入文件系统上的文件来绘制图形。C ++开发人员通过将Vector容器与自定义分配器结合使用,可以将文件映射到内存中,从而非常优雅地解决了这一罕见而难以理解的障碍。这样,像素可以像数组一样进行读写。

注意我并不是说Delphi可以针对Nintendo DS。我只是指出面对异常数据表示时向量容器的灵活性和强大性。

动态分配器

动态与普通Delphi数组存储信息的方式大致相同。

这种分配器类型没有关于如何将项目存储在内存中的标准。只要分配器可以交付每个项目,向量容器就不会在乎细节。

这与排序模型的正好相反,排序模型要求以可预测的方式存储项目。

在Delphi中,最接近动态分配器的匹配项是常规TArray <>。

定制分配器

如果向量容器仅使用顺序或动态分配器进行操作,则与普通列表或集合相比,它们的用处将微不足道。向量容器的强大之处在于它们允许任何人实现自己的分配器,从而可以处理几乎任何来源或介质中的数据序列,而不会破坏接口。这是节省时间的功能所在。

如果某些东西可以表示为值的列表或序列,则可以为其编写分配器;因此-可以通过公共向量接口访问数据。

这是一些可以实现的分配器建议。最终,每个开发人员都要编写一个适合自己特定情况的分配器类:

  • 将数据库记录映射到Pascal记录的分配器
  • 与基于磁盘的阵列一起运行的分配器
  • 与JSON或XML节点一起使用的分配器
  • 使用内存映射的分配器
  • 包装目录列表的分配器
  • 包装文本文件的分配器

不用说,一旦您将Delphi强大的RTTI添加到方程式中,事情就会变得很有趣。将颈椎枕数据库字段映射到记录的向量列表将很容易创建。就ORM(对象关系映射)而言,避免类实例并直接映射到Pascal记录类型将更快,内存效率更高。因此,此材料中有一些未开发且引人入胜的机会。

一个实际的例子

在查看实际的矢量类实现之前,让我们看一下如何使用它。由于顺序分配器非常适合在单个内存块中维护记录(在处理低级任务时可能非常强大),让我们开始吧。

type
 
  TTest = record
    id: integer;
    name: shortstring;
    class function Create(id: integer; name: shortstring): TTest; static;
  end;
 
  TMyVectorList = class(TVector<TTest>)
  end;

在上面的代码中,我们定义了一个带有几个字段的简单记录结构。为了使生活更轻松,我们还定义了一个函数来初始化记录,这样我们就不会在源代码中添加太多样板代码。helper函数的实现很简单:

class function TTest.Create(id: integer; name: shortstring): TTest;
begin
  result.id := id;
  result.name := name;
end;

您会注意到,我们还定义了一个类TMyVectorList,该类仅从TVector继承为我们的记录类型。我们真的不需要这样做。您可以只创建TVector <TTest>的实例,但是如果您想扩展带有某些实用程序功能的矢量容器,则将其隔离可以使代码更易于阅读和使用。

接下来,切换到窗体设计器,并在窗体上放置一个TButton和一个TListbox。尽管对于我们的示例而言并不重要,但您可能需要将列表框的锚点设置为左,上,右和下,以使其随窗体调整大小。

 完成表单设计后,双击按钮并填写以下代码:

procedure TForm1.Button1Click(Sender: TObject);
var
  x: integer;
  lList: TMyVectorList;
begin
  lList := TMyVectorList.Create(vaSequence);
  try
    // populate
    for x := 1 to 10 do
    begin
      lList.add( TTest.Create(x, 'name #' + IntToStr(x-1) ) );
    end;
 
    // do some inserts
    lList.insert(4, TTest.Create(4, 'first') );
    lList.insert(9, TTest.Create(9, 'second') );
 
    // Read back
    for x := 0 to lList.count -1 do
    begin
      listbox1.items.add( lList.Items[x].name );
    end;
 
  finally
    lList.Clear;
    lList.free;
  end;
end;   

 按下F9键(编译并运行),然后单击填充按钮,结果应为:

有什么好处?

如果您想知道为什么上述内容与使用标准TArray或TList实现相同之处有什么不同,则必须记住分配器的工作方式。这里的区别不是它做什么,而是如何做。您必须考虑到,向量列表的功能不是容纳记录列表的能力,而是它提供了一个统一的接口来按顺序访问值-不论其来源或格式。

TVector不了解列表数据的来源或来源。它只知道通用类型(在我们的示例中为TTest)和用于管理列表的分配器。除此之外,TVector只是一个统一的访问点。

在上面的示例中,我们使用了顺序分配器,因此列表项存储在单个内存块中。

为确保内存完整性一致,让我们仔细检查!让我们使用指针直接访问旧式的内存!

访问原始数据

在我们的下一个示例中,我们将访问向量包含的原始内存(或更准确地说,分配器提供的向量公开)。我谦虚的vector实现使容器和分配器方便地解耦,仅受接口引用约束。但是该接口公开了一些方法,这些方法使我们可以利用数据(当然适用时)。

有问题的方法是:

  • LockMemory(可变数据)
  • UnlockMemory()
  • GetDataSize()

我要强调的是,在实现自己的向量分配器类时,如果无法或不允许进行内存访问,则应引发异常。我非常不愿意完全包含LockMemory()和UnLockMemory(),因为容器的全部目的是抽象化内容的知识。但是,实话实说,所有这一切都需要实践。

因此,切换回表单设计器,让我们添加另一个按钮,如下所示:

 

 接下来,我们定义记录类型的指针变体:

  PTest = ^TTest;
  TTest = record
    id: integer;
    name: shortstring;
    class function Create(id: integer; name: shortstring): TTest; static;
  end;  

 定义了记录的指针类型后,我们可以直接从内存地址访问记录。现在,双击新按钮并填写以下代码:

procedure TForm1.Button2Click(Sender: TObject);
var
  x: integer;
  lList: TMyVectorList;
  lRaw: PTest;
begin
  lList := TMyVectorList.Create(vaSequence);
  try
    // populate
    for x := 1 to 10 do
    begin
      lList.add( TTest.Create(x, 'name #' + IntToStr(x-1) ) );
    end;
 
    // Get a pointer to the managed memory
    lRaw := nil;
    llist.Memory.LockMemory(lRaw);
    try
      // manually read out the name field from each
      for x := 0 to lList.Count-1 do
      begin
        listbox1.items.add( lRaw^.Name );
        inc(lRaw);
      end;
    finally
      lList.Memory.UnLockMemory;
    end;
 
  finally
    lList.Clear;
    lList.free;
  end;
end; 

 按下F9键(编译并运行),单击“新建”按钮,您应该看到以下结果:

如您所见,顺序分配器可保持数据完整性并提供可预测的结果。

无数的支持

原始的C ++向量标准类开箱即用地支持枚举器,但是C ++标准库中定义枚举器的方式与Delphi不兼容。因此,我没有实现与Delphi不兼容的枚举器模式,而是自然选择了传统的Delphi枚举器系统。与C ++向量列表100%兼容会很酷,但是在兼容和可用之间有一条细线。我总是站在实践的一边。

因此,TVector类继承自TEnumerable,因此您可以方便,现代的方式访问列表,例如:

var el: TTest;
for el in lVectorList do
begin
  // do something with el
end;

坚持不懈

使用从TEnumerable继承的向量列表,您可能想知道为什么我要舍弃TPersistence的好处而转向枚举器?向量列表实现了标准的Assign()方法,因此您可以在向量容器之间分配内容。它还将检查分配器模型是否是顺序的-并在可能的情况下快速调整大小并移动整个内存块。

我还确保分配器基类继承自泰山颈椎枕TInterfacedPersistent,因此以后在此方面还有很大的改进空间。读者,我将把这一部分留给您。

Delphi Vector向量类单元

我认为到目前为止,我们已经涵盖了足够多的理论和用例,因此现在该检查代码了。从字面上看。

由于代码覆盖了20个A4页面,因此我自由地在BitBucket上建立了一个存储库。该代码是根据MPL v2(Mozilla公共许可证)发布的,因此您可以在自己的项目中安全地使用它。

您可以在此处下载或存储库:

https://bitbucket.org/cipher_diaz/pasvector/src/master/

该存储库包含一个Delphi和Freepascal示例。虽然在面向Delphi的生态系统中包含Freepascal似乎很奇怪,但该代码是为同时使用TMS Web Framework(使用Freepascal将Delphi代码转换为JavaScript)的Delphi开发人员提供的。

全屏

猜你喜欢

转载自www.cnblogs.com/taishanlaofu/p/12710871.html