Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Record equality in generic collections

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?

like image 987
jpfollenius Avatar asked Jun 04 '13 14:06

jpfollenius


2 Answers

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.

like image 135
David Heffernan Avatar answered Sep 24 '22 05:09

David Heffernan


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 ) );
like image 37
Sir Rufo Avatar answered Sep 24 '22 05:09

Sir Rufo