Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a generic TList of records

I am trying to write a generic TList that contains records of a specific type. Starting from David's answer on this question, I have written this class:

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;

Having methods that return a pointer to the record works well (not perfectly) as pointers to records can be used as if they were records in most use cases. Using a record with properties and setters, these test cases work as expected:

  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;

Problem 1 - modifying a contained record

... this test case compiles and runs but does not work as desired:

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

Modifications are applied to temporary copy of the record and not to the one stored in the list. I know this is a known and expected behaviour, as documented in other questions.

But... is there a way around it? I was thinking that maybe if the TMERecordList<>.Items property had a setter the compiler could maybe do what is actually desired. Could it? I know David has got a solution, as hinted at in this question... but I can't seem to find it on my own.

This would really be nice to have, as it would allow me to have a way of using the list identical (or almost) to that of a TList of objects. Having the same interface means I could easily change from objects to records and viceversa, when the need arises.

Problem 2 - interface ambiguity

Having the TList<> return a record pointer does pose some interface ambiguity problems. Some TList<> methods accept T parameters, and we know that being records, these are going to be passed by value. So what should these methods do? Should I rethink them? I'm talking specifically about these sets of methods:

  • Remove and RemoveItem
  • Extract and ExtractItem
  • Contains IndexOf, IndexOfItem and LastIndexOf

There is some ambiguity as to how these should test contained items to see if they match the parameter record value. The list could very well contain identical records and this could become a source of bugs in user code.

I tried not deriving it from TList<>, so as not to have these methods, but it was a mess. I couldn't write a class similar to TList without also writing my own TListHelper. Unfortunately System.Generics.Collections's one has some needed fields that are private, like FCount, and cannot be used outside the unit.

like image 554
Frazz Avatar asked Aug 29 '16 07:08

Frazz


2 Answers

Problem 1

Your Items property is not marked as being default. Hence your erroneous code is picking up the base class default property. Add the default keyword to your Items property:

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

Problem 2

This is really a consequence of deriving from TList<T>. I would not recommend doing that. Encapsulate an instance of TList<T> and therefore define the interface explicitly rather than inheriting it. Or implement the list functionality directly in your code. After all, it's not much more than a wrapper around a dynamic array.

For what it is worth my classes don't use TList<T> at all which is a decision that I was very happy with when Emba broke the class in a recent release.

like image 137
David Heffernan Avatar answered Nov 21 '22 22:11

David Heffernan


In the recent versions TList<T> in System.Generics.Collections contains a List property that gives you direct access to the backing array of the list. You can use that to manipulate the records inside the list.

like image 37
Stefan Glienke Avatar answered Nov 21 '22 21:11

Stefan Glienke