编写一个记录类型(record)的通用TList

7

我正在尝试编写一个通用的TList,其中包含特定类型的记录。从David在question上的答案开始,我编写了这个类:

Type
  TMERecordList<T> = Class(TList<T>)
  Public Type
    P = ^T;
  Private
    Function GetItem(Index: Integer): P;
  Public
    Procedure Assign(Source: TMERecordList<T>); Virtual;
    Function First: P; Inline;
    Function Last: P; Inline;
    Property Items[Index: Integer]: P Read GetItem;
  End;

Procedure TMERecordList<T>.Assign(Source: TMERecordList<T>);
Var
  SrcItem: T;
Begin
  Clear;
  For SrcItem In Source Do
    Add(SrcItem);
End;

Function TMERecordList<T>.First: P;
Begin
  Result := Items[0];
End;

Function TMERecordList<T>.GetItem(Index: Integer): P;
Begin
  If (Index < 0) Or (Index >= Count) Then
    Raise EArgumentOutOfRangeException.CreateRes(@SArgumentOutOfRange);
  Result := @List[Index];
End;

Function TMERecordList<T>.Last: P;
Begin
  Result := Items[Count - 1];
End;

拥有返回指向记录的方法非常好(不是完美的),因为在大多数情况下,可以像使用记录一样使用指向记录的指针。使用具有属性和设置器的记录,这些测试用例按预期工作:

  TMETestRecord = Record
  Private
    FID:     Word;
    FText:   String;
    FValues: TIntegers;
    Procedure SetID(Const Value: Word);
    Procedure SetText(Const Value: String);
    Procedure SetValues(Const Value: TIntegers);
  Public
    Property ID: Word Read FID Write SetID;
    Property Text: String Read FText Write SetText;
    Property Values: TIntegers Read FValues Write SetValues;
  End;

  // TestSetItem1
  rl2[0] := rl1[0];

  // TestSetItem2
  r.ID     := 9;
  r.Text   := 'XXXX';
  r.Values := [9, 99, 999, 9999];
  rl1[0]   := r;

  // TestAssignEmpty (rl0 is empty... after assign so should rl2)
  rl2.Assign(rl0);

  // TestAssignDeepCopies (modifications after assign should not affect both records)
  rl2.Assign(rl1);
  r.ID     := 9;
  r.Text   := 'XXXX';
  r.Values := [9, 99, 999, 9999];
  rl1[0]   := r;

问题1 - 修改包含的记录

...这个测试用例可以编译和运行,但是没有按照预期工作:

  // TestSetItemFields
  rl1[0].ID     := 9;
  rl1[0].Text   := 'XXXX';
  rl1[0].Values := [9, 99, 999, 9999];

修改应用于记录的临时副本,而不是存储在列表中的记录。我知道这是已知和预期的行为,如其他问题所述。

但是...有没有办法规避它?我想也许如果TMERecordList<>.Items属性有一个setter,编译器可能会做实际需要的事情。可以吗?我知道David有一个解决方案,就像在这个question中暗示的那样...但我自己似乎找不到它。

这真的很好,因为它将使我能够以与对象的TList几乎相同的方式使用列表。具有相同接口意味着我可以轻松地在对象和记录之间进行更改,当需要时。

问题2-接口歧义

TList<>返回记录指针确实会导致一些接口歧义问题。一些TList<>方法接受T参数,我们知道作为记录,这些参数将通过值传递。那么这些方法应该做什么?我该重新考虑它们吗?我特别谈论这些方法组:

  • Remove和RemoveItem
  • Extract和ExtractItem
  • 包含IndexOf、IndexOfItem和LastIndexOf

关于这些方法如何测试包含的项是否与参数记录值匹配存在一些不确定性。列表中可能会包含相同的记录,这可能会成为用户代码中的错误源。

我尝试不从TList<>派生它,以免有这些方法,但是这很混乱。我不能编写一个类似于TList的类,而不编写自己的TListHelper。不幸的是,System.Generics.Collections的TListHelper有一些需要的私有字段,例如FCount,不能在单元外使用。


编写一个简单的通用容器并不难,也不需要像Emba的混乱列表助手那样的东西。而且它仍然存在漏洞。 - David Heffernan
修改 rl1.List[0] 而不是 rl1[0]。而且你没有默认属性的事实确实使得代码使用了继承的索引属性(据我所知,这实际上不会有任何影响)。 - Rudy Velthuis
当使用指向TList元素的指针时,您必须意识到这些指针是易变的。当列表增长并被重新分配时,指针将变为无效。在列表中使用记录可能有效,但是记录的分配需要像对象一样明确地完成,而TList应该持有分配的记录的指针,而不是列表中的元素的指针。 - Paul Michael
2个回答

4

问题 1

你的 Items 属性没有标记为默认值。因此,你错误的代码会选择基类的默认属性。将 default 关键字添加到你的 Items 属性中:

property Items[Index: Integer]: P read GetItem; default;

问题2

这实际上是从TList<T>派生的结果。我不建议这样做。封装TList<T>的一个实例,并明确定义接口,而不是继承它。或直接在代码中实现列表功能。毕竟,它只是一个动态数组的包装器。

值得一提的是,我的类根本不使用TList<T>,这是一个决定,在Emba最近的版本中,该类遭受了破坏,但我对此感到非常满意。


关于问题1...很好的发现,但是它不起作用,因为我的Items属性没有提供setter,所以是只读的。我会实现一个并重试。关于问题2...我不知道rtl实现有缺陷,但你肯定是对的,一旦我找到时间,我可能会编写自己的实现。现在重要的是提供一个稳定的接口,让其他开发人员可以开始使用它。谢谢。你非常有帮助。 - Frazz
1
"Emba在最近的版本中破坏了类。" 嗯? - Arioch 'The
1
@Rudy,在XE8中存在许多不同的问题。 随后的版本改善了情况。 显然Emba使用的测试套件有缺陷。 我希望它已经得到改进,但我找不到任何人与我谈论这个问题。 - David Heffernan
@David:你有什么东西可以让我测试吗?我正在尝试调试和改进这个,但缺乏能显示错误的适当数据。我认为我可以让它们接受调试后的源代码。只需通过velthuis at gmail dot com与我联系即可。我知道还存在问题。TList<T>和其子类在RTL和视觉库中被广泛使用,因此它们应该在所有情况下都能很好地工作。 - Rudy Velthuis
该类旨在将Items设计为只读属性,返回内部项目的地址。如果您想要能够编写List[i] := SomeRec,那么这种设计不适合您。 - David Heffernan
显示剩余3条评论

2
在最近的版本中,System.Generics.Collections 中的 TList<T> 包含一个 List 属性,它可以直接访问列表的后备数组。您可以使用它来操作列表内部的记录。

2
我会并且内部使用它。将其设置为“Protected”即足够。如其他地方所指出的,列表具有比计数更多的项,没有范围检查。将其公开给用户作为“Public”在我看来是危险的。 - Frazz

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