generic TList of records with a sub list?
Asked Answered
C

1

5

I want to use a generic TList of records with a sub list in Delphi XE5:

type
  TMyRecord=record
    Value1: Real;
    SubList: TList<Integer>;
  end;

  TMyListOfRecords=TList<TMyRecord>;

var
  MyListOfRecords: TMyListOfRecords;

Assignments to the field of the records are not possible:

MyListOfRecords[0].Value1:=2.24; 

or

MyListOfRecords[0].SubList:=TList<Integer>.Create;

will result in "left side cannot be assigned to" error by the compiler.

See also: How to modify TList<record> value?

The following workaround works:

AMyRecord:=MyListOfRecords[0];
AMyRecord.Value1:=2.24;
AMyRecord.SubList:=TList<Integer>.Create;
AMyRecord.SubList.Add(33);
MyListOfRecords[0]:=AMyRecord;

Because of performance issues I would like to avoid to copy the data to the temporary AMyrecord. I would rather like to access the record fields and the sub list directly.

What is the best way to handle this?

Cobos answered 6/3, 2014 at 1:6 Comment(0)
S
6

The list exposes its internal storage, which is a dynamic array, through the List property. So you can write:

MyListOfRecords.List[0].Value1 := 2.24; 

Whether this makes any measurable difference in performance in comparison to the alternative with value copies, I cannot tell. It would be worthwhile checking that.

As @LURD correctly says, List returns the internal storage. And this may have more than Count elements. Specifically it has Capacity elements. So, if you use it, you must access the elements using array indexing, over elements 0 to Count-1. Remember also that modifications to the size of the list may involve a re-allocation and so the internal storage may move. Any reference you take to List is only valid until the next re-allocation.

These warnings should suggest to you that you only consider using List if performance constraints demand so. And even then, use it sparingly.

In my codebase, I have an alternative to TList<T> whose Items[] property returns a pointer to the element. The container still stores as a dynamic array for efficient memory layout. I preferred this option to the List property because I felt it led to cleaner code.


OK, you asked to take a look at my list class that returns pointers to the elements. Here it is:

type
  TReferenceList<T> = class(TBaseValueList<T>)
  type
    P = ^T;
  private
    function GetItem(Index: Integer): P;
  public
    property Items[Index: Integer]: P read GetItem; default;
  public
    // .... helper types for enumerators excised
  public
    function GetEnumerator: TEnumerator;
    function Enumerator(Forwards: Boolean): TEnumeratorFactory;
    function ReverseEnumerator: TEnumeratorFactory;
    function IndexedEnumerator: TIndexedEnumeratorFactory;
  end;

Now, some explanation needed. The base class, TBaseValueList<T> is my alternative to TList<T>. You could substitute TList<T> if you wish. I don't because my base class does not have an Items property. That's because I want the specialized classes to introduce it. My other specialization is:

type
  TValueList<T> = class(TBaseValueList<T>)
  private
    function GetItem(Index: Integer): T;
    procedure SetItem(Index: Integer; const Value: T);
  public
    property Items[Index: Integer]: T read GetItem write SetItem; default;
  end;

The implementation of my TBaseValueList<T> is pretty obvious. It's very similar to TList<T>. I don't think you really need to see any of the implementation. It's all very obvious.

As a simple way to get a reference to an element, you could wrap List up like this:

type
  TMyList<T> = class(TList<T>)
  public
    type
      P = ^T;
  private
    function GetRef(Index: Integer): P;
  public
    property Ref[Index: Integer]: P read GetRef;
  end;

function TMyList<T>.GetRef(Index: Integer): P;
begin
  Result := @List[Index];
end;

If you want a richer set of containers than is provided by Delphi, you might care to look at Spring4D. Although I'm not sure if they have anything like my container that returns references.

Sane answered 6/3, 2014 at 7:10 Comment(8)
Note that the List property dynamic array may be larger than MyListOfRecords.Count. So don't pass it to routines iterating all elements.Flybynight
@LURD Thanks. FWIW, I think List prop really is the answer to the question that was asked mind you. It's the only way to get direct element access with vanilla TList<T>.Sane
@David Heffernan Thanks! would it possible to see your alternative to TList<T> and how you use it? After searching around a lot about this subject I think this might help a lot of people.Cobos
@Cobos It's pretty much the same as TList<T>. It doesn't have an Items property. That's the main difference. You could take the code from TList<T>, and remove Items, GetItem and SetItem, and you'd be pretty close. The enumerators are quite cool too. I don't particularly want to show all the code in a compilable form. I'm not sure the world needs yet another set of containers. You should be able to follow these ideas yourself from what I've given you. I hope. You should also double check whether or not perf is an issue.Sane
You could even subclass TList<T> and add a property that returns a reference to an item using the List property that I described. That would be the simple way to do it. Or even do that with a class helper.Sane
@Cobos I made another edit to show a simple way to make accessing references cleanerSane
@David Heffernan First of all I would like to say that I am impressed by all your effort you put in answering the question. Thank you very much. What I don't understand: What is the difference between MyListOfRecords.List[0].Value1 := 2.24; and if I you your last TMyList with MyListOfRecords.Ref[0]^.Value1 := 2.24; ?Cobos
There is no difference. Feel free to use the former!Sane

© 2022 - 2024 — McMap. All rights reserved.