delphirecorddelphi-11-alexandriaiequalitycomparer

Why does the default equality comparer for records behave differently when the record contains certain fields and is accessed by list index?


Using TEqualityComparer<T>.Default for record types seems to "work" in some situations ("work" as in checking for value-equivalence across all fields between two records), but not in others. I'm sure that writing a custom comparer is a better alternative over using this one, but I'm curious as to what's causing the difference.

Specifically, it seems like if the record has a String field, calling Equals() on the two records themselves works, but accessing them inside a list does not, eg:

type
  TestRec = record
    Value: String;
  end;
begin
  var Rec1: TestRec;
  var Rec2: TestRec;
  Rec1.Value := 'a';
  Rec2.Value := 'a';

  var List1: TArray<TestRec> := [Rec1];
  var List2: TArray<TestRec> := [Rec2];

  var Comparer: IEqualityComparer<TestRec>;
  Comparer := TEqualityComparer<TestRec>.Default;

  Comparer.Equals(Rec1, Rec2); // returns true
  Comparer.Equals(List1[0], List2[0]); // returns false
end;

This doesn't seem to happen when the record doesn't contain managed types like String, so I'm guessing it's something related to memory since the default comparer uses CompareMem().


Solution

  • Since your record type has a managed String field, the Comparer should not try to compare the raw memory of the two record instances, as it would be comparing the String internal data pointers, not comparing the character data they point at.

    But that is exactly what is happening in your example. In reality, TEqualityComparer does raw memory comparisons, and so it only works with records that contain trivial non-managed fields, and have the same padding bytes in the record layout.

    When you compare Rec1 and Rec2 directly, they compare as equal only because the two Strings happen to be pointing at the same memory block for the 'a' literal. Change one of the Strings to a different value and they will not compare as equal anymore since their data pointers will be different. You can also force this in your example by calling UniqueString() on the Strings, eg:

    Rec1.Value := 'a';
    Rec2.Value := 'a';
    UniqueString(Rec1.Value);
    UniqueString(Rec2.Value);
    ...
    Comparer.Equals(Rec1, Rec2); // returns false!
    

    When you compare the List elements, the two Strings are pointing at separate memory blocks because the 'a' data gets copied when each array is created, which is why the elements never compare as equal regardless of their values.

    So, to compare a record type with managed fields, you MUST use a custom Comparer that compares the record members one-by-one as appropriate for their types.