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;
... 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.
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:
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With