【问题标题】:Separate Data Structure vs VirtualStringTree's PVirtualNodes to Store Data?单独的数据结构与 VirtualStringTree 的 PVirtualNodes 来存储数据?
【发布时间】:2011-05-13 14:00:46
【问题描述】:

所以我一直在搞乱创建自己的单独数据结构。我终于让它工作了,但后来我发现与旧方法相比,内存使用率高得离谱。

为了对此进行测试,我创建了相同的测试应用,但我会将数据存储在我的 PVirtualNodes 中。

当添加 1000 个根,每个根有 1000 个子节点时,单独的数据结构使用大约 208 MB,而 PVirtualNode 仅使用大约 160 MB,而且速度也快了一点。

我认为使用单独的数据结构应该使用更少的内存,并且速度更快,但我想这就是代价?

这里是“在 PVirtualNode 中存储数据”的来源:http://pastebin.com/j6L2eHJt

这里是“将数据存储在单独的数据结构中”的来源:http://pastebin.com/iSwR0hW1

【问题讨论】:

  • 还要确保测量正确的内存。不是来自任务管理器的“内存使用”或“工作集”,而是“VM 大小”或“提交大小”
  • 或者更好的是使用 AQTime 直接测量您的 VCL 对象内存使用情况。

标签: delphi data-structures memory-management virtualtreeview


【解决方案1】:

我看到你的分离数据结构代码的第一个问题是它错误地使类别拥有用户。这意味着,如果同一个人属于两个类别,那么它将在您的数据结构中出现,就好像有两个完全不同的人一样。你会在项目的整个生命周期中都后悔那个决定。

您需要一个用户列表,并且您需要一个类别列表。一个用户可以包含一个它所属的类别列表,或者一个类别可以包含一个属于它的用户列表。也许两者兼而有之。

type
  TUser = class;
  TCategory = class;
  TContactList = class
  private
    FUsers: TObjectList<TUser>;
    FCategories: TObjectList<TCategory>;
  end;

  TUser = class
  private
    FCategories: TObjectList<TCategory>;
  public
    constructor Create(const DisplayName: string; SkypeID: Integer);
    property DisplayName: string;
    property SkypeID: Integer;
    property Categories: TObjectList<TCategory> read FCategories;
  end;

  TCategory = class
  private
    FUsers: TObjectList<TUser>;
  public
    constructor Create(const Name: string; ID: Integer);
    property Name: string;
    property ID: Integer;
    property Users: TObjectList<TUser> read FUsers;
  end;

constructor TContactList.Create;
begin
  // The contact list is the single master list of all contacts; it
  // owns the user objects, so set OwnsObjects = True
  FUsers := TObjectList<TUser>.Create(True);
  FCategories := TObjectList<TCategory>.Create(True);
end;

constructor TUser.Create;
begin
  // A user does not own its categories; set OwnsObjects = False
  FCategories := TObjectList<TCategory>.Create(False);
end;

constructor TCategory.Create;
begin
  // A category does not own its members; set OwnsObjects = False
  FUsers := TObjectList<TUser>.Create(False);
end;

注意这段代码没有提到树控件。联系人列表不拥有树控件。如果确实如此,那么您就会回到几个月前开始的地方,那里有the problem of how to display users in multiple tree controls。还要注意,用户可以出现在多个类别中,但没有一个类别拥有该用户。相反,联系人列表拥有用户,并授予类别权限以引用他们,反之亦然。

您开始这个项目时遇到的第一个问题是树控件中的how to detect duplicate elements。现在这个问题要容易得多,因为根本没有树控制。您只需在平面用户列表中查找重复项。如果您一开始不向该列表添加重复项,那么您不必再担心在更复杂的 GUI 控件中查找重复项。

请注意,数据结构不是树。这是两个列表,列表中的每个项目都可以引用对面列表中的任意数量的项目。从这个意义上说,它实际上是一个图表。您只需将数据显示为一棵树,否则很难将其可视化。

那么,既然您有一个独立于树控件的数据结构,那么如何将树链接到它应该显示的数据?每个节点都应该持有对它所代表的TUserTCategory 的引用。节点的数据记录可以这样定义:

type
  PNodeData = ^TNodeData;
  TNodeData = record
  case Integer of
    0: Obj: TObject;
    1: User: TUser;
    2: Category: TCategory;
  end;

您可以使用它来实现树的OnGetText 事件,如下所示:

procedure TJeffForm.TreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
  Data: PNodeData;
begin
  if TextType = ttStatic then
    Exit;
  Data := Sender.GetNodeData(Node);
  Assert(Assigned(Data), 'Node wasn''t initialized properly');
  Assert(Assigned(Data.Obj), 'Node has no object');

  if Data.Obj is TUser then
    CellText := Data.User.DisplayName
  else if Data.Obj is TCategory then
    CellText := Data.Category.Name
  else
    CellText := Format('Unknown node type %s', [Data.Obj.ClassName]);
end;

也就是说,每个节点将包含一个用户或一个类别。数组的第一个元素将包含一个值,它是其中一种类型,但由于您事先不知道它将是哪种类型,所以您有第三种类型,可以安全地用作任何一种类型,TObject。在记录的第一个字段中包含所需的数据很重要,因为这是您在调用 AddNewNode 时允许设置的字段,即使在节点完全初始化之前也是如此。

避免长时间创建节点的秘诀之一是避免创建不需要的节点。如果顶级节点被折叠,那么您实际上不需要创建它的任何子节点。只需在顶部节点上设置标志,表明 个子节点,它就会用“+”按钮正确地绘制自己。如果用户稍后单击按钮以展开节点,则树控件将询问您 有多少 个子节点,此时它将创建它们。即使这样,它也只会初始化需要立即绘制的节点。推迟工作,直到有必要为止。拥有一百万个联系人的人可能永远不想同时查看所有联系人,因此无需在您的 GUI 控件中创建一百万个项目。

当你的程序启动时,你首先要做的就是加载用户和类别列表,然后设置树的类别计数:

Tree.RootNodeCount := ContactList.Categories.Count;

就是这样。

树的事件将负责初始化阶段的其余部分。如果您希望从一开始就扩展某些类别节点,那么您所要做的就是扩展它们。树的事件会问你每个节点有多少孩子,你可以实现事件来回答。一旦树为它们创建了节点,它会询问如何初始化它们,您可以在那时将相应的用户或类别对象分配给节点。树会在需要更多信息时告诉您。你不必给它比它要求的更多的东西。

【讨论】:

  • 奇怪,我相信我已经回复了这个。无论如何,这里是:感谢您的详细回答!请问,为什么我要将TCategorys 存储在TUser 对象中,并且 还将TUsers 存储在TCategory 对象中?那不会占用更多内存吗?
  • 杰夫,给定一个用户,你怎么知道它属于哪个类别?对于每个用户,使用的额外内存大约为:TObjectList.InstanceSize + Categories.Count * SizeOf(Pointer)。可能有多少个类别?任何一位用户可能属于多少个类别?正如我所说,任何一个列表都是可选的。预期用途应确定数据结构。如果您不打算为用户查找类别,则不需要该列表。
  • @Rob - 好吧,这一切都取决于最终用户如何使用我的软件。我认识一个拥有 100 多个类别的人,其中很多人都在其中。但无论如何,我做错了什么,是我让我的对象列表拥有这些项目吗?我应该在用户所在的每个类别中存储指向用户的指针吗?我该怎么做?
  • 正如我在第一句话中所说的,你做错的是你让类别拥有用户。 (无论这是否是通过 TObjectList 的所有权概念专门完成的,都是无关紧要的)。这意味着两个类别的用户将有两个单独的 TUser 实例。是的,每个类别都应该引用该类别中的每个用户。这就是我给你的代码中TCategory.Users 属性的用途。要将用户添加到某个类别,请致电 Category.Users.Add(User)(如果您同时保留两个列表,也可以致电 User.Categories.Add(Category))。
  • Grate answer,我特别喜欢 Jeff 旧问题的后面引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多