Assume you have a record with an overloaded equality operator
TSomeRecord = record
Value : String;
class operator Equal(Left, Right : TSomeRecord) : Boolean;
end;
(implementation compares string values). If adding two records to the list that are equal based on the overloaded operator I would expect the Contains
method to return true in both cases. But in fact, the generic list seems to just compare the memory content of records instead of applying the overloaded equality operator.
var
List : TList <TSomeRecord>;
Record1,
Record2 : TSomeRecord;
begin
Record1.Value := 'ABC';
Record2.Value := 'ABC';
List.Add(Record1);
Assert(List.Contains(Record1));
Assert(List.Contains(Record2)); // <--- this is not true
end;
Is this the expected behaviour? Any explanations?
Assuming that you did not specify a comparer in the constructor to TList.Create
you will get TComparer<TSomeRecord>.Default
as your comparer. And that is a comparer that performs simple binary comparison using CompareMem
.
That's fine for a record full of value types, with no padding. But otherwise you will need to supply your own compare function when you instantiate the list.
If you want to look at the details, the default comparer for records is implemented in Generics.Defaults
. For larger records the equality comparer is this function:
function Equals_Binary(Inst: PSimpleInstance; const Left, Right): Boolean;
begin
Result := CompareMem(@Left, @Right, Inst^.Size);
end;
For smaller records there is an optimization and your comparer will be the 4 byte comparer. That looks like this:
function Equals_I4(Inst: Pointer; const Left, Right: Integer): Boolean;
begin
Result := Left = Right;
end;
That's a bit weird, but it interprets the 4 bytes of your record as a 4 byte integer and performs integer equality comparison. In other words, the same as CompareMem
, but more efficient.
The comparer that you want to use might look like this:
TComparer<TSomeRecord>.Construct(
function const Left, Right: TSomeRecord): Integer
begin
Result := CompareStr(Left.Value, Right.Value);
end;
)
Use CompareText
if you want case insensitive, and so on. I've used an ordered comparison function because that's what TList<T>
wants.
The fact that the default record comparison is an equality comparison tells you that attempts to sort lists of records without specifying your own comparer will have unexpected results.
Given that the default comparer uses an equality comparison tells you that it would not be totally unreasonable to use a comparer like this:
TComparer<TSomeRecord>.Construct(
function const Left, Right: TSomeRecord): Integer
begin
Result := ord(not (Left = Right));
end;
)
That will be fine for unordered operations like IndexOf
or Contains
but obviously no use at all for sorting, binary search and so on.
To get the expected behavior you have to create the List with a comparer.
In this case you should use
List := TList<TSomeRecord>.Create( TComparer<TSomeRecord>.Construct(
function ( const L, R : TSomeRecord ) : Integer
begin
Result := CompareStr( L.Value, R.Value );
end ) );
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